kumadori というgemを公開しました。
はじめに
仕事でRubyを使うようになってから5ヶ月程、前々から自分のgemをつくって公開したいと考えていました。
非常に小さい単機能のgemですが、今回はじめて自作gemを作って公開できたのでそのご報告をしたいと思います。
kumadoriとはなにか?
kumadoriはRubyでデコレーターパターンをシンプルに実装するためのgemになります。
デコレーターパターンとはプログラミングのデザインパターンのひとつで、既存のオブジェクトにデコレーターオブジェクトをラップすることで柔軟にオブジェクトの機能拡張を行なうことが出来る手法です。
kumadoriという名前ですが、歌舞伎の化粧のことを隈取(くまどり)ということから命名しました。
個人的にソフトウェアの名前に日本語が使われているのが結構好きなので、デコレーターを表すなんか良い感じの日本語ないかな〜って考えた所、 デコレーター -> 装飾者 -> おしゃれする -> 化粧 -> 歌舞伎の化粧を「隈取」という無理な連想ゲームでkumadoriという名前にしました。
使い方
kumadoriはデコレーションの対象となるインスタンスのClass名によって自動的に使用するDecoratorクラスを決定します。
例えばUserクラスのインスタンスをデコレーションするのはUserDecoratorクラス、Animalクラスのインスタンスのデコレーションを行なうのはAnimalDecoratorクラスのような感じになります。 そのため、使用するDecoratorクラスを予めKumadori::BaseDecoratorクラスを継承して定義して下さい。
インスタンスをデコレーションするときはKumadori.decorate(instance)メソッドを使います。
以下に参考コードを記載します。
# # Basic ruby class. # class User attr_accesstor :first_name, :last_name def initialize(first_name, last_name) self.first_name = first_name self.last_name = last_name end end # # Decorator class for User instance. # class UserDecorator < Kumadori::BaseDecorator def full_name "#{self.last_name} #{self.first_name}" end end user = User.new('Yuji', 'Arakaki') # user instance decorated by UserDecorator decorated_user = Kumadori.decorate(user) decorated_user.full_name # => "Arakaki Yuji"
もしデコレーションするインスタンスに該当するDecoratorクラスがない場合、Kumadori::BaseDecoratorクラスによってデコレーションされます。
そのため、もしどのオブジェクトにも共通して追加させたいメソッドなどがあれば、Kumadori::BaseDecoratorクラスをOverrideして定義してください。
class Animal end module Kumadori class BaseDecorator < ::SimpleDelegator def live? true end end end animal = Animal.new # Because of AnimalDecorator class is not defiend, # it is decorated by Kumadori::BaseDecorator decorated_animal = Kumadori.decorate(animal) decorated_animal.live? # => true
配列などのCollection内に入っている各要素をすべてデコレーションしたい場合はKumadori.collection_decorate(collection)を使ってください。
Collection内に入っている要素をすべてデコレーションした新しいCollectionを返します。
members = [] members << User.new('Kanoko', 'Higa') members << User.new('Ai', 'Kawasaki') members << User.new('Takeo', 'Kikuchi') decorated_members = Kumadori.collection_decorate(members) decorated_members.map{ |user| user.full_name} # => ["Higa Kanoko", "Kawasaki Ai", "Kikuchi Takeo"]
このGemの使いドコロ
僕がこのGemを作ったのは、Railsでモデルに格納されているデータをフォーマットして表示する処理を定義するときに、表示に関する処理はドメインロジックではないのであまりモデル内には記載したくないが記述する良い場所がないのでDecoratorというレイヤーを作りたいと考えたからです。
Decoratorというレイヤーを入れることで、モデル記述していた表示に関するメソッドが整理できるのでより見通しが良くなり、変更もDocoratorを変更すればモデル自体を変更せずに済むのでより柔軟性が高まります。
RailsにDocoratorというレイヤーを導入するGemはactive_decoratorやdraperという有名どころのgemもあるのですが、どちらもRailsのプラグインになっているためRails依存しない形式でデコレーターを簡単に使えるgemにすればすぐに実装出来るしより使いやすいと思ってkumadoriを作ってみました。(まあ、あとで調べるとそういうgemは山ほどあったのですが・・)
おわりに
超絶シンプルなgemですが、人生初のOSSを公開したということでほんの少しテンションが上がっています^^
OSSということでコミットメッセージやREADMEも英語で書いたり、テストどれくらい書けば良いか考えたりして色々楽しかったです。
これからも小さいgemでも大きいWebサービスでも良いので色々開発して定期的にアウトプット出来るように頑張りますd^^
MacBookProのメモリを増設
メモリ増設開始!
フレームワークなくとも秩序は成り立つ ~ フロントエンドJavaScript ~
はじめに
いま開発しているWebアプリケーションでフロントエンドのJSを書こうとしているときに、以前の開発でAngularJSを使用していた経験があることもあり、自然にどのフレームワークを採用しようかと色々物色しておりました。
AngularJSの他にもEmberやKnockout, Backbornなど調査してみたんですが、どのフレームワークも「SPA」(シングルページアプリケーション)を作ることを前提に作られているようでした。
自分が開発中のアプリケーションはSPAではないし、AngularJSを採用したプロジェクトの時もバックエンドで採用しているフレームワークとの相性とかで結構苦労したのでどうしようかな〜と考えて、っていうかフロントエンドMV*フレームワークいる?って所から考えなおしてみました。
フロントエンドMV*フレームワークを使いたい理由
もちろんチームやプロダクトの性質によると思いますが、僕が考えた上では
のような感じかな〜と思いました。
適当にJSでゴリゴリ書いちゃうとどのファイルに何があるかがわからずに見通しが悪くなるし、JSはUIをいじることに使われる特性上デザインの変更に対して対応出来ないといけないので変更ができるだけ簡単に行えるようにしておかないといけない。 さらにあわよくばデータバインディングやフィルタやらの力を借りて効率よく開発したいって感じです。
忘れちゃならないトレードオフ
ただ、フレームワークの採用にはもちろん良いことだけでなく、以下のようなトレードオフも存在します。
- 学習コスト
- フレームワークへの依存
学習コスト
そのフレームワークを使いこなせるようになるとどんどん開発効率が上がっていきます。
ただそのフレームワークを使いこなすまでにはそのフレームワーク独特の概念やAPIを学習するのにそれなりの時間がかかり、さらにそれを使いこなすためにも時間がかかります。
さらにフレームワークを使うと必ずそのフレームワーク独特の問題などぶつかることも多く、それを解決するのに余計な時間がかかってしまう可能性も考慮する必要があります。
フレームワークへの依存
フレームワークはあらゆるアプリケーションに使えるわけではなく、そのフレームワークが想定しているユースケースがあります。 そのユースケースに沿って使えば効率良く開発が行えますが、そこから外れたことをやろうとすると余計なHackが必要になってしまいます。
そのためフレームワークの選定を間違えると余計なコストがかかったり、わざわざフレームワークに合わせたアプリケーションになってしまうという恐れもあります。
必要ないなら使わない方が良い
今回私が行っているWebアプリケーション開発の状況は以下のような感じでした。
- このアプリケーション開発以外のプロジェクトもやってる
- アプリケーションはSPAじゃない
- インフラ、バックエンド、フロントエンド、ネイティブアプリまで全てを担当するためフロントエンドの専任エンジニアはいない
ざっくり言うと、「やることはたくさんあるからフレームワークの選定ミスで余計なコストを掛けたくない!」っていう状況です。
その状況のなかで考えたときに
- フレームワークの機能での効率化は今回の場合学習コスト + 相性の悪さで多分チャラになる
- でも見境なくJS書いてカオスにもしたくない。
- 自分である程度JSの構造化をすればいいんじゃね?
っていうことになりました。
おれおれのフロントエンドJS設計
フロントエンドJavaScriptで書くコードをざっくり以下の様に分離します。
- Model: ビジネスロジックの管理
- Template: HTMLの生成
- ViewModel: イベントハンドリングやデータフロー
ディレクトリ構造は以下のような感じになります。
├── javascripts │ ├── models │ │ └── user.js │ ├── templates │ │ └── errors_message_template.js │ │ └── day_options_template.js │ └── viewmodels │ ├── profile_viewmodel.js │ └── top_viewmodel.js
Model (ビジネスロジック)
MVCフレームワークでもモデルと呼ばれ 、アプリケーション特有のロジックを入れるレイヤーです。 たとえばユーザープロフィールの登録ページを実装する場合、Userというモデルオブジェクトを作り、そこにバリデーションの条件やバックエンドのAPIとの通信などのコードを書きます。
ここにあるコードはデザインとの結びつきがなく、どのページでも使いまわせるようにしておきます。
参考コードとして下記のUserモデルをのせておきます。
function User(name, email, job) { var self = this; self.name = name: self.email = email; self.job = job self.nameIsBlank = function(){ self.name.length === 0; } }
Template(HTMLの生成)
入力フォームをチェックしてエラーメッセージを表示したり、Ajaxやユーザーのアクションに反応して変化するHTMLを生成するコードです。 例えば日付に関する入力フォームで年、月、日のセレクトボックスがある場合、年、月が変更されると日のセレクトボックスのoptionタグはその月にある日の数に合わせて変化しないといけません(8月なら31個、2月なら28個のように)。
それを実現するために年と月の値からoptionタグを必要な数だけ生成するコードを書きます。
ここは同じようなHTMLが必要な箇所があれば上手く使いまわせるように引数によって動的な値を管理します。
以下に年と月を引数に受け取って必要なoptionタグを生成するTemplateのコードを記載します。
/* * 引数のyearとmonthからその月の開始日と終了日までのoptionタグを生成する。 * selectedDayに指定した日付のoptionタグにはselected="selected"の属性を設定する。 * 例: * <option value="1">1</option> * <option value="2">2</option> * <option value="3" selected="selected">3</option> * <option value="4">4</option> * <option value="5">5</option> * . * . * <option value="31">31</option> * */ function DayOptionsTemplate(year, month, selectedDay){ var self = this; var jsMonth = month - 1; var monthFirstDate = new Date(year, jsMonth, 1); var monthLastDate = new Date(year, jsMonth + 1, 0); self.template = generateTemplate(monthFirstDate.getDate(), monthLastDate.getDate(), selectedDay); function generateTemplate(firstday, lastDay, selectedDay){ function a_day_option_template(day, selected){ var attr = ''; if (selected === true){ attr += ' selected="selected"'; } var template = '<option value="' + String(day) + '"' + attr + '>' + String(day) + '</option>'; return template; } var template = ''; for(var i=firstday; i<=lastDay; i++) { var selected = i === selectedDay ? true : false; template += a_day_option_template(i, selected); } return template; } }
ViewModel(イベントハンドリング・データフロー)
HTMLの要素に対してイベントをつけたり、モデルの変更をキャッチした結果生成したHTMLを実際のDOMに差し込んだりする所です。
ここの部分が一番DOMに結びつきやすく、使い回しも効きにくいため、Webアプリケーションのページ毎に作成するイメージです。
DOMと結びついた面倒な部分をここに押し込めることで他のModelやTemplateの独立性を高めています。
以下にプロフィール登録ページのViewModelのサンプルコードを記載します。
ProfileViewModel = function(){ var self = this; $(document).ready(function(){ // イベントハンドリング $('form[name="profile" button.submit]').click(function(){ // データフロー // Formの値をモデルに与える var name = $('input[name="user[name]"').val(); var email = $('input[name="user[email]"').val(); var job = $('input[name="user[job]"').val(); var user = new User(name, email, job); if (user.nameIsBlank === true) alert('名前を入力して下さい'); return false end }); // イベントハンドリング $('select[name='month']').change(function(){ // データフロー // 選択されている値を元にテンプレートを作成する var year = $('select[name='year']').val(); var month = $('select[name='month']').val(); var day = $('select[name='day']').val(); var dayOptions = DayOptionsTemplate(year, month, day); // テンプレートの差し込み if ($("select[name='day']").children().length > 0){ $("select[name='day']").empty(); } $("select[name='day']").prepend(dayOptions.template); }) }); }
このViewModelを使いたいページで実行すれば、必要な処理がすべて用意されるという感じです。
ViewModelの実行はscriptタグで行っても良いですが、dispatcherを作ってURLで必要なVIewModelをセットすると より見通しさらによくなります。
dispatcher("^/profile", function(){ ProfileViewModel(); }); function dispatcher(path, func){ dispatcher.path_func = dispatcher.path_func = dispatcher.path_func || [] if (func) return dispatcher.path_func.push([path, func]); for(var i = 0, l = dispatcher.path_func.length; i<l; ++i ){ var func = dispatcher.path_func[i]; var match = path.match(func[0]); match && func[1](match) }; }; dispatcher(location.pathname);
終わりに
とりあえずJSがカオスにならずに秩序をもって構造化でき、ある程度使い回しが可能なコードが作れるような設計を、フレームワークなしで実現できるように取り組んでみました。
いままさに開発中のアプリケーションでは結構気持ちよくこの設計が使えていますが、まだまだ解決したい問題は多々あります。
たとえば分離したJSファイルの依存性を解決したり、Templateが純粋なJSなのでデザイナーが触りづらいなど色々あります。
その解決方法も目処は立っている(依存性解決はWebpackを使ったり、Templateもそれを解決してくれそうなライブラリがある)のですが、いまは自分達のプロジェクトでは回っているので問題が顕在化しそうになってから取り組めば良いかな〜という感じです。
必要なコストが払えるのであればフレームワークを導入するのも良いですが、 自分達のプロジェクトに最適化した設計を行なうことが出来るのであればその方が効率は上がるし、プログラミングをしていてもかなり楽しくなるのでおすすめします^^
APIファーストでバックエンドとフロントエンドを別々に開発する時にハマるクロスドメインアクセス
いま個人で開発を行っているWebサービスがありまして、そこではバックエンドをRubyonRails、フロントエンドをClojure & ClojureScriptで開発し、APIでお互いやりとりするように設計しています。(その設計した理由は色々ありますが、単純に好きな2つの言語を使いたかったのが大きな理由の一つですw)
おそらくこれからのアプリケーション開発において、このようにフロントエンドとバックエンドをサーバーも言語も分けて設計・開発することが多くなると思いますので、最近ハマった問題をシェアしておきます。
同一生成元ポリシー(Same Origin Policy)
そのハマった問題というのが同一生成元ポリシーによるAjaxの規制です。
同一生成元ポリシーとは、ドメインAに配置されているHTMLファイルから別ドメインBのサーバーのAPIにAjaxで通信することが出来ないという制約です。
この制約があることで、例えば開発中にフロントエンドをlocalhost:8000で起動し、バックエンドをlocalhost:8001で起動してAjaxでAPI通信しようとするとエラーが発生してしまいます。
では、フロントエンドから別サーバーのAPIと通信するにはどうすればいいのか・・
CORS(Cross-Origin Resource Sharing)
実はこういう希望に対処するためにCross-Origin Resource Sharingという、XMLHttpRequest(Ajaxのことです)でクロスドメインアクセスを実現する仕様をW3C勧告で各ブラウザが実装しています。
CORSではクロスドメインアクセスを行うクライアント側とアクセスされるサーバー側の振る舞いが仕様で規定されています。
その概要はブラウザとサーバーがHTTPヘッダを使ってアクセス制御に関する情報をやりとりしてアクセスを行うものです。
CORSを実際に行う場合にクライアント側のJSで特別な制御を行う必要はあまりないのですが、サーバー側で以下のようなアクセス制限に関するルールを設定する必要があります。
では、これからどのようにCORSを行っているかを説明していきます。
CORSの動作 - クライアント側
クライアント側がクロスドメインアクセスを行うときに行う通信手段には2パターンあります。
以下に記載する条件にすべて該当する場合はpreflightリクエストを送る必要がないと判断して、直接サーバーにリクエストを送信します。逆にいうと、この条件に合わなければ必ずpreflightリクエストを送るということです。(これらの判断はすべてブラウザが行ってくれるのでクライアント側で特別にコードを書く必要はありません。)
preflightリクエストを送信しない条件
- HTTPメソッドがGET, POST, HEADのどれか
- HTTPヘッダにAccept, Accept-Language, Content-Language, Content-Type以外のフィールドが含まれていない
- Content-Typeの値がapplication/x-www-form-urlencoded, multipart/form-data, text/plainのいずれか
上記の条件をみて頂けると気づくと思いますが、APIでデータをやりとりするときはjsonやxmlをフォーマットとして使うことが多いので、大半はまず間違いなくpreflightリクエストが送られるということです。^^;
CORSの動作 - サーバー側
Originのチェック
サーバー側では、まず受け取ったリクエストがクロスドメインから受け取ったリクエストか判断するためにリクエストヘッダに含まれるOriginフィールドの値が入っているかチェックします。
Originフィールドに値が入っていれば、そのドメインがCORSを許可したドメインかをチェックして受け入れるか否かを判断します。
preflightリクエストかどうかを判断する
次にサーバーはこのリクエストが通常のリクエストかpreflightリクエストかを判断しないといけません。実際には以下の項目をチェックしてpreflightかどうかを判断します。
ちなみに、僕はクロスドメインでエラーが発生したリクエストを調査しているときに、POSTで送ったはずのHTTPメソッドがOPTIONSになっていて思わずライブラリのバグかなと中のコードを30分程探ってしまいました(- -;)
アクセス許可メソッドのチェック
続いて、HTTPメソッドがアクセスを許可しているものかどうかをチェックします。
preflightリクエストが来ていた場合、Access-Control-Request-Methodに本来送るはずのHTTPメソッドがここに記載されているので、この中身もみてアクセスを許可するかどうかを判断します。
レスポンスヘッダの作成
Access-Control-Allow-Origin
CORSが成立して、リクエストに対してレスポンスを返す時にレスポンスヘッダのAccess-Control-Allow-Originフィールドにクロスドメインアクセスが許可されるドメイン名を付加します。
もしAccess-Control-Allow-Originのフィールドがない場合はブラウザはクロスドメインアクセスに失敗したと判断してエラーを発生させます。
リクエストがpreflightリクエストだった場合は他にもレスポンスヘッダを追加します。
Access-Control-Allow-Methods
preflight後の実際のリクエストで利用を許可するHTTPメソッドの情報を付加します。
Access-Control-Allow-Headers
preflights後の実際のリクエストで利用を許可するリクエストヘッダの情報を付加します。
Access-Control-Max-Age
preflightの結果をキャッシュする時間を付加します。毎回毎回クロスドメインアクセスを行うたびにpreflightリクエストを送ると送受信コストが無駄にかかってしまうため、こちらで指定した時間はその結果をキャッシュしブラウザ側はpreflightリクエストを送らずに直接リクエストを送信します。単位は秒で指定します。
理屈は分かった。で、どうすればいいんだ!?
上の説明でCORSを行う仕組みはなんとなく把握できたと思うのですが、じゃあ具体的にどういう対応すればいいのでしょうか?
自分の場合はバックエンド側にはRubyonRailsを使っており、幸いCORS対応を行うためのgemでrack-corsというのがすでにあるのでそちらを使いました。
rack-corsのREADMEを見れば良いのですが、Gemfileにrack-corsを追記してbundle installしたあとに、config/application.rbに以下のように記載します。
module YourApp class Application < Rails::Application # ... config.middleware.insert_before 0, "Rack::Cors" do allow do origins 'your_app_domain' resource '*', :headers => :any, :methods => [:get, :post, :put, :delete, :options] end end end end
自己紹介です。はじめまして、arakajiです。
ブログ開設しました。arakajiです。
私は現在24歳で、沖縄在住のアマチュアプログラマーです。プログラミングをはじめて約10ヶ月になり、触っている言語はPythonとRubyなんですがメインでRubyを使ってます。
アルゴリズムを知るためにJavaやJavaScriptのコードはたまに読むんだけど、ruby, pythonで
入ってるから色んな宣言が多くてびっくりしたね!でも、近いうちそういう言語も覚えないといけないかなー。
使っているOSはLinuxのUbuntuです。最初の半年以上はwindows7でやってたんだけど、rubyで色々やろうとすると引っかかる引っかかる(TーT)それで一念発起して恐る恐るUbuntuを入れてみたら・・・・なんということでしょう!
rubyの扱いがぐっ!!と楽になるではありませんか。
gemも簡単にインストールできるし、文字コードでの苦労もなくなるし。rubyでプログラミングするならやっぱLinux!っていうネットの意見を聞いて正解でした。ただいつかMacも使ってみて、なぜAppleファンがあんなに熱狂的なのか感じたいっていう希望もあります。
さて、このブログですが、基本プログラミングや覚えた技術について書くつもりです。
でも、他にも書きたいことがあったら書くつもりです。
良いプログラマーはブログをしている人が多いということで始めたので、自分のために書くのがメイン!それプラス、ここでの発信がすこしでも素晴らしきハッカー文化に貢献できたら幸いです。
なので、出来る範囲で更新していくので気軽に見にきてくださいねv^_^
P.S
いまプロプログラマーになろうと就活中なので、興味をもって頂けたら連絡頂けると嬉しいです。
興味をもって頂けなくても、求人に応募してくるかもしれないですし、なにかの機会にお会いことがあれば、よろしくおねがいします。