書いた人: 浦嶌 啓太 (ursm)
Rails での開発には様々なベストプラクティスがあります。例えば「プレゼンテーションロジックはビューではなくヘルパーに置く」「ドメインロジックはなるべくモデルに寄せる」などです。これらは総称して「Rails のレールに乗る」と表現されたりもします。
しかし残念なことに、Rails のレールに乗るだけですべてがうまくいくわけではありません。細心の注意を払って書いていたはずのコードが、時が経つにつれて肥大化し、醜くなっていく – これをお読みの皆さんも同様の経験をお持ちではないでしょうか。
筆者は QA@IT というサービスの開発に携わるにあたり、Rails での開発において陥りやすい罠について分析し、それらを解決するためにいくつかの試みを行いました。本稿はそこから得られた知見を共有することを目的として書かれています。
この記事の内容は札幌 Ruby 会議 2012 の同名のセッションをベースにしています。
それでは、Rails アプリケーションのコードベースが崩壊していく要因を考えてみましょう。今回は以下の 3 点を題材とします:
ロジックが散在したビューはとかく読み難く、メンテンスし難いものです。ではそのロジックをヘルパーに追い出しましょう……となるわけですが、ナイーブにやっているとヘルパーメソッドがどんどん増えていきます。その数 3 桁ともなると、似ているようで微妙に違うメソッドや、多分どこからも使われていないけれど怖くて消せないメソッドなどが現れてきて、新しくメソッドを足すにも「どこに書けばいいかよくわからないから一番下でいいや」という感じになってきます。ヘルパークラスを分割してみたり、メソッド名にプレフィックスを付けてみたりするも焼け石に水で、管理不能な状態に陥ります。
Rails では再利用可能なテンプレートとしてパーシャルを用います。これはシンプルなケースでは適切に機能しますが、少し複雑なことをしようとすると途端に辛くなってきます。
例として「最近サインアップしたユーザの一覧を表示するパーシャル」を考えてみましょう。SNS のサイドバーによくあるアレですね。
表示するユーザを取得する部分がいかにも臭いますね。例えばログインユーザの有無によってクエリを切り替えるような仕組みになった場合、このパーシャルにロジックを足すのでしょうか。ここは大人しくコントローラに初期化ロジックを書いてみます。
うーん、users#show に本筋とは関係ないロジックが入ってしまいました。また、このパーシャルを使うアクションすべてにこのコードをコピペしろというのでしょうか。もちろんそんなことはなくて、フィルターを使えばいいですね。
これで万事解決……なのでしょうか? 実際のところ、あまり状況は変わっていません。パーシャルと初期化ロジックは分離したままですし、このパーシャルを使うアクションでは忘れずにフィルターを適用しなければなりません。ひとつの関心事に対して複数の箇所を気にしないといけないというのはいかにも嫌な感じがします。このようなパーシャルとフィルターの組み合わせが増えてくると、もはや把握し切れるものではありません。
よく言われる “Skinny Controller, Fat Model” (ロジックをなるべくモデルに寄せて、コントローラの処理を減らす) を忠実に守ると、主要なモデルがどんどん大きくなっていきます。そのモデルに特有のロジックなら仕方がないとも思えるのですが、ある特定のアクションでしか使われないロジックや、複数のモデルに跨がるロジックなども全部ごちゃ混ぜになっていて、どれがどれだかさっぱりわからない状態になります。
さて、これらの問題にどうやって対処すればよいのでしょうか。
ヘルパーが抱える主な問題点は「数が増えてくると管理し切れない」ことでした。ヘルパーは構造化の仕組みを持たないため、すべてのメソッドがフラットになってしまいます。これを何らかの基準で分離できれば状況は改善されるはずです。
膨れ上がったヘルパーを眺めていると、大半がモデルを引数に取るメソッドであることに気が付きます。思考実験として、モデルのインスタンスメソッドとしてこれらを定義してみてはどうでしょうか。
なかなか悪くありません。あるモデルに特有のプレゼンテーションロジックを自身に持たせるというのは発想として自然ですし、分離の基準としても明快です。
とはいえおもむろにモデルにメソッドを定義するのは乱暴に過ぎますので、Presenter または Decorator と呼ばれる種類のライブラリを使います。代表的な実装として ActiveDecorator や Draper があります。両者ともビューのコンテキストにおいてモデルにメソッドを追加する機能を持ちます (相違点については後述します)。
実際の利用イメージを ActiveDecorator の例で見てみます。モデルのクラス名 + Decorator という名前のモジュールを用意しておくと、モデルオブジェクトがビューに受け渡されるタイミングで自動的に extend され、モジュールに定義したメソッドが使えるようになります。
これにより、モデル特有のプレゼンテーションロジックはそれぞれモデルに対応するモジュールに分離できるようになりました。もうヘルパーメソッドの先頭にモデル名のプレフィックスを付けなくていいのです ;)
Draper も実現することは基本的には同じですが、方式に違いがあります。ActiveDecorator はモデルオブジェクトにモジュールを extend するのに対し、Draper はモデルオブジェクトをラップするオブジェクトを作ってメソッドを移譲します。Draper の方式ではメソッドを追加する前とした後でオブジェクトの identity が変化してしまうため、よくよく注意しないとハマります。
パーシャルとそれに付随するロジックをどのように管理するかという問いに対して、ひとつの答えとなるのが Cells というフレームワークです。
Cells は大雑把に言うとパーシャルにコントローラをくっ付けたようなものです。先ほど例に挙げた「最近サインアップしたユーザの一覧を表示するパーシャル」を Cells に置き換えてみましょう。
RecentSignedUpUsersCell はコントローラに相当するもので、cell と呼びます。定義した cell はパーシャルのように任意のビューから呼び出すことができます。
これでパーシャルと付随するロジックをひとつの論理的なまとまりとして扱うことができるようになりました。
なお、Cells の姉妹品として Apotomo というフレームワークもあります。これは Cells を拡張してリクエストを受け取れるようにする野心的な試みなのですが、いかんせん野心的すぎて開発がストップしてしまっているようです。
主要なモデルは大抵複数の機能を持っています。例えば QA@IT の Question (質問) モデルには、タグ付け・バージョン管理・vote (+1 / -1) などの様々な機能があります。これらをモデルにガシガシ書いていくと、あっという間に巨大なモデルができあがります。
ひとつの機能には、それを実現するためのメソッドはもちろん、association や validation、scope の定義などのコードが必要です。ベタに書くとこんな感じでしょうか。
タグ付けの機能を構成するコードが上から下まで散らばってしまっています。後から見たとき、どこからどこまでがひとつのまとまりなのか把握するのは難しいでしょう。
この問題に対するシンプルな改善策として、機能のまとまりをモジュールに分割してしまうのが効果的です。先ほどのコードを例にすると、こんな感じになります。
効果は一目瞭然ですね。
複数のモデルがやり取りをしてひとつの機能を構成するような場合、そのロジックはどこに置けばよいのでしょうか。「ユーザが商品を買う」というロジックはユーザと商品、どちらの責務だと思いますか? 色々な考え方があると思いますが、私たちのチームでは「インタラクションをオブジェクトにする」という方法を取りました。1
例えば、QA@IT には「質問者が有用と思った回答を accept する」という機能があります。これは以下のような一連のロジックで構成されています。
これを分解して各モデルに分散させるのではなく、「回答を accept する」というひとつのまとまりとして表現するためにはどうしたらよいかを考えた結果、以下のようになりました。コードそのものの説明は本筋ではないため割愛させていただきますが、上記のリストと比較していただければなんとなくご理解いただけるのではないでしょうか。
ひとつの論理的なまとまりであるはずのロジックが様々な場所に分散していてわかりにくいと感じるようなら、このようなアプローチを試してみるといいかもしれません。
Rails のレールがまだ及んでいない real world problem に対して、私たちがどのように立ち向かってきたかをご紹介しました。
本稿で挙げた解決案はまだまだ不十分なものですし、他にも問題はいくらでもあると思います。本稿が問題を考えるきっかけとなれば幸いです。
浦嶌 啓太 (ursm)
(株)永和システムマネジメント サービスプロバイディング事業部チーフプログラマ。好きな Linux ディストリビューションは Funtoo Linux。
そもそもの発想には DCI (Data Context Interaction) アーキテクチャの影響を強く受けていますが、実装にあたり原型を留めないほどアレンジしているので、共通点はほぼないものと考えてください。 ↩