TL;DR
- MVCもレイヤで捉えて関係性の設計をするといいのでは
- 普通のRubyオブジェクトを積極的に使いたいですね
- 「パーフェクト Rails」に期待しましょう
長くなって面倒くさくなり、途中から手抜き感が半端ないですが許してください
この記事の位置付けなど
- 7 Patterns to Refactor Fat ActiveRecord Models - Code Climate Blog [翻訳]
- エリック・エヴァンスのドメイン駆動設計
- エンタープライズ アプリケーションアーキテクチャパターン
これらの参考文献を踏まえてRailsアプリケーションのリファクタリングをしていて、だいぶ方向性や考え方がまとまってきたので、これからチームに合流する人を想定読者に、Qiitaがどんな感じで作られているのかを文書化したものです。(参考文献の一覧は記事の最後にあります)
内容的には文献[2,3]を踏まえて、文献[1]を咀嚼し部分的に変更を加えたものといった感じであり、何かを1から新しく提案するものではありません。もっとこうした方がいいとか、うちではこうしてる、みたいな意見を貰えると嬉しいです。
本文は大雑把に2部構成になっています。
- 背後にある思想的な話
- 実際にどういう風にやっているか
Railsで実践していることがベースになっていますが、DjangoやSymfonyなどのYet Another RailsなMVC WAFでも通じる話なんじゃないかと思います。
それと、当たり前ですがここでいうMVCとはRailsとかで使われている新しい方のやつのことであり、パロアルト研究所で作られたGUIアプリ向けのやつのことではありません。
背後にある思想的な話
レイヤアーキテクチャ
ふわっと説明すると、レイヤアーキテクチャというのはアプリケーションを責務に応じたいくつかの層としてとらえる設計手法のことです。このとき上の層が下の層を一方的に利用するようにすることで、オブジェクト間の結合を疎に保ち、ドメインロジックの凝集度を高めることができます。詳しくは文献[2]の前半を参照してください。
とりあえず押さえるべき点は
- 各オブジェクトはいずれかの層に属し、複数の層にまたがることはない
- 層の関係は一方通行であり、相互参照する関係は層をまたがない
ということです。
4つの層
4つの層に分けられます。上から順に
レイヤ | 役割 |
---|---|
ユーザインタフェース層 | ユーザに情報を表示し、ユーザの入力を解釈する。 |
アプリケーション層 | ドメイン層のオブジェクトを協調させる。ビジネスに関する知識を持たず、作業を調整するだけ。 |
ドメイン層 | いわゆるビジネスロジックを表現する。一番重要な層。 |
インフラ層 | 上位レイヤを支える一般的な技術。MySQLとかRedisとか。 |
下の層は上の層のことを知っていてはいけない、というのはインフラ層を見れば納得感がありますね。MySQLが特定のアプリケーションのために設計されたものだったら困ってしまいます。
で、MVCをこの4層に当てはめると以下のように対応付けされます。
レイヤ | MVC |
---|---|
ユーザインタフェース層 | View |
アプリケーション層 | Controller |
ドメイン層 | Model |
つまり我々は次のことを強く意識してコードを書かねばなりません。
- ModelはControllerやViewでどう呼び出されるか知るべきではない
- ControllerはViewがどのように描画するか知るべきではない
MVCというとModelとViewとControllerが互いに連携しあってアプリケーションを記述する、みたいな気がしますが実際はModelは一方的に使われるだけということになります。
MVCをレイヤにあてはめることに合理性はあるのか
どうしてもMVCをレイヤにあてはめて考えると無理が生じる場面はあります。例えばViewの中でどういう内容がレンダリングされるのか知らなければ、Conrollerでデータを用意してViewに渡すことなどできないわけです(そもそも「渡す」という表現をしている時点で層に分かれていないとも言える)。
というわけで厳密に言えばMVCをレイヤに当てはめることはできないのだけど、心構えとしてはそこに層があると捉えることで「このオブジェクトはアプリケーション層のオブジェクトだから、インタフェース層の情報を極力排除しよう」みたいな議論ができるしいいのでは?ということです。
でも考えようによっては、MVCが厳密に層に分かれていないのはフレームワーク側の都合だ、と言えなくもないような気がしています。
レイヤが分離できていないとどうなるか
というわけで綺麗にバランス良く層に分かれていればいいのですが、多くのフレームワークがそのように分離することに強い制約を持っているわけではないので、またプログラムを書く人の力量に任せられているので、意識していないとつい特定の層が肥大化してしまいがちです。
- Fat View
- ユーザインタフェース層にアプリケーションやドメイン層のロジックが詰め込まれた状態
- Viewから直接Modelを触っているとn+1問題が頻出する
- Fat Controller
- MVCアーキテクチャでWebアプリケーションを書くと誰もが最初に陥る
- ドメイン層のロジックがアプリケーション層に書かれている状態で、一つの変更をするのに複数箇所変更しないといけなくなっていたりする
- Fat Model
- なんでもかんでもModelに詰め込んでいる状態
- アプリケーション層のロジックまでModelに含まれていることも多い
おそらく多くの人が初めはFat Controllerなアプリを書き、そしてFat Modelに至ります。
Fat Viewはどうすればいいか
Fat Viewはテンプレートの中にif-else文などがもりもり出てきて何がなんだかわけが分からなくなっているような状況です。ロジックをアプリケーション層とドメイン層に適切に移動させ、意味のある単位でパーシャルに切り分けなければなりません。
ロジックの移動については後述するとして、
どういうものをパーシャルに切り出せばいいのだろうか、ということについては「BEMのBlock単位でパーシャルにする」というのが指標になるかも知れません。BEMについてはググってください。
ついでに、細かい話ですが、CSSファイルもBlock単位で分割することで、パーシャルとCSSファイルが一対一対応するようになります。こうしておくとパーシャルを消す際に一緒に消すべきCSSが明確になるので、使ってないCSSが大量に残ってる!みたいなことになりにくくなってちょっぴり幸せです。
Fat Modelを分割すればいいというものではない
Fat Model問題に対して、「Fat Modelを分割しましょう」と主張するのは簡単ですが、問題は「Modelの大きさ」というよりは 「ドメイン層にくるべきでないロジックが含まれている」 という方が適切だろうと思います。
小さく分割することだけに囚われた結果、オブジェクトの責務の凝集度が下がってしまったり断片化しては元も子もありません。結局のところ、オブジェクトが単一の責任を負うように設計しましょう、互いに疎に保ちましょう、というあたりまえな話に戻ってくるわけです。
ちなみに文献[2]ではこのような責務が断片化された状態のことをティア化と読んでいます。レイヤもティアも辞書を引くと「層」という意味ですが、使い分けるようです。
Active RecordとRow Data Gateway
Modelにドメイン層のロジックだけが残っているなら、実装が大きくてもいいのかというとそうではなく、責務に応じて適切に分割していく必要がある、というのは既に述べました。
ところで、RailsのActiveRecord
のもとになっているのが、参考文献[3]に出てくるActive Recordパターンです。同じ章にはRow Data Gatewayパターンというのもでてきて、DBの1レコードを1インスタンスで表す点は共通、ドメインロジックを含むか含まないかという違いがあります。つまり、Active Recordにするか、Row Data GatewayにするかはModelをどこまで細かく分割するかという話に通じると言えます。(このあたりのことは参考文献[6]でも説明されています。)
純粋なRow Data Gatewayを追い求めるのは、せっかくのActiveModel
の機能を飼い殺しにすることになるのでオススメしませんが、積極的に責務のまとまりを外部に切り出して個々のModelを軽くすべきです。(ただしティア化しないように注意)
RailsにはModelのバリデーションやコールバックを外部オブジェクトに切り出す機能があります。この点については参考文献[4]が詳しい。
ドメインオブジェクトの種類とライフサイクル
ドメイン層を記述するときには以下のパターンとそのライフサイクルがあると考えています。詳しくは参考文献[2]の第5, 6章を読んでください。ここで書いているのはほぼ参考文献そのままです。
モデルを表現するパターン
名前 | 役割 |
---|---|
エンティティ | 別の実装をまたいでも追跡されるような連続性と一意性を持ったもの。同一性を持つ。 |
値オブジェクト | 他の何かの状態を記述する属性であり、同一性を持たない。 |
サービス | オブジェクトより手続きとして表現したほうが明確なドメイン。エンティティや値オブジェクトに責務を押し付けないほうが適切なものがここにくる。 |
以上の3つのオブジェクトに対し、オブジェクトのライフサイクルを通じて整合性を維持し、管理が複雑だとしてもモデル自身はシンプルに保つ必要があります。そしてライフサイクルの変遷をカプセル化し、ドメインを綺麗に保つために以下の3つの役割を導入します。
名前 | 役割 |
---|---|
集約 | 明確な所有権と境界を定義する。整合性を保つ上で決定的に重要な役割を持つ。 |
ファクトリ | オブジェクトの生成・集約をカプセル化する。ライフサイクルの始まり。 |
リポジトリ | 永続化されたオブジェクトにアクセスする手段を提供する。 |
ActiveRecord
を使っているとこれらが全て一つのmodelの中に詰め込まれる事態が発生しがちです。が、それでなんとかなるのは小規模サイトまででしょう。
実際にどういう風にやっているか
Railsだとappディレクトリ以下にそのままずばりな名前のディレクトリがあるので錯覚を覚えますが、MVCとそれらが一対一対応しなければならないということは無いですし、むしろ複数ディレクトリで各層を形成すると考えるべきでしょう。
結論から書くとこんな感じに分割しています。
層 | 名前 |
---|---|
ユーザインタフェース層 (View) |
views view_objects |
アプリケーション層 (Controller) |
controllers observers decorators parameters |
ドメイン層 (Model) |
models mailers callbacks validators policies queries services value_objects factories |
ユーザインタフェース層
views
言わずと知れたHTMLをレンダリングするためのやつです。
view_objects
これは参考文献[1]に出てくる同名のクラスとほぼ同じです。
下の層は上の層のことを知るべきではないので、modelは特定のviewと紐づくメソッドを持つべきではありません。かと言ってviewの中でごりごりロジックを書くのはヤバイので、その間の橋渡しをするのがview_objectsです。
例えばQiita:Teamのエクスプローラにはタグツリーが表示されます。ドメイン層で見ると複数のTag
が存在しているだけです。これを使いやすい形に変換してviewとmodelの汚染を防ぐのがview_objectsの役目です。
アプリケーション層
controllers
省略(だんだん書くのがだるくなってきたぞ...)
observers
Railsにはバージョン3.?までObserverという機能がcoreにありましたが、今ではrails-observers gemに切りだされました。詳しい経緯は追っていませんが、おそらくは後ででてくるcallbacksと役割が被るからというのがcoreから排除された主な理由なんじゃないかと想像します。
ではなぜそのcallbacksと合わせてobserversも使っているのかというと、observersはアプリケーション層にいる(という風に区別して使っている)からです。observersがいることでcallbacksがドメインロジックに専念することができます。
observersに書かれるものには例えば社内ツールへの通知とかキャッシュへの追加・削除みたいな、modelの特定のタイミングで実行したいがドメインロジックではないようなコードです。つまりobserversを全てoffにしてもアプリケーションとしての振る舞いには大きな支障をきたさないということです。
decorators
参考文献[1]にも同名のパターンが出てきますが、あれとだいたい同じです。違いは、特定条件下でのみ実行されるobserversの処理を切り出して軽くするためのものであって、callbacksをリファクタリングするものではない、たとえその処理が特定のcontrollerの特定のactionでのみ発生するものなのだとしても、それがビジネス的に重要であればそれはドメイン層に存在すべきだ、という事です。
parameters
参考文献[1]にformsオブジェクトパターンというのが出てきて、その利点として
Formオブジェクトを導入するメリットとして、ActiveRecord自身の中でvalidationを行なうという融通の効かない方法に代えて、validationロジックをコンテキストに応じた場所で定義できるというのがあります。
と言われていますが、アプリケーション層とドメイン層がformsの中で入り交じっていて嫌な感じがします。変わりに参考文献[5]で出てきたparametersと後述するfactoriesを使っています。
parametersはユーザが入力したデータをmodelsやfactoriesに都合がいい形に変換するラッパのような存在です。ちょうどview_objectsがやっていることの反対のことをするのがparametersです。
ドメイン層
models
超重要なやつです。なんでもかんでもこいつに突っ込むのは間違いだからどうにかしよう、というのはこの記事と参考文献[1]の主張するところです。
(書くの疲れたもうやめたい)
mailers
メール送るやつです。ドメイン層と捉えるべきかアプリケーション層と捉えるべきか悩ましい感じですが、callbacksの中から呼び出すことがあるので、ドメイン層に分類しています。
callbacks
modelのbefore_xxx
とかafter_yyy
とかを委譲するのがこのcallbacksです。前述のobserversとは違い、ドメインロジックを表す重要な役割があります。
このときItem
モデルに紐づくからItemCallback
, ItemObserver
だとやってしまいがちですが、そうするのは短絡的でよろしくない。初めのうちはそれでいいかも知れませんが、大きくなってきたら意味のあるまとまりに分割しましょう。
詳しくは参考文献[4]参照。
validators
validatorsはmodelsのバリデーション機能を切り出すものです。modelsがデータとその振る舞いをカプセル化し、validatorsはそのデータについての制約を記述する場所です。
詳しくは参考文献[4]参照。
policies
policiesという名前から参考文献[1]のやつと同じかと思われるかも知れませんが、実体は微妙に違います。やっている責務は同じですが。
ユーザの権限管理にcancan gemを使っています。DSLの可読性がいいです。cancanはabilityというオブジェクトを作ってそこにユーザの権限を書いていくのですが、管理対象が増えてくると肥大化して辛い感じになります。
policiesはabilityにあったロジックを関連するmodelsに持たせることを目的としたオブジェクトです。abilityの中でuserと対象オブジェクトのpolicyから個別の権限があるかないかを判断する、という構造です。なので、policiesはuserとmodelsの間の関係を記述するための場所、と言えます。
ちなみにcancanは開発が止まっているので、forkされたcancancanを使った方がいいです。
queries
これは上で書いたリポジトリに相当するものです。ActiveRecord
使うとmodelのクラスメソッドとして実装することが多いですがqueriesに切り出します。参考文献[1]のやつとだいたい同じ。
services
ドメインを形成する大事な処理だけど、modelやvalue_objectに持たせるのが不適当なものがここに来ます。といっても割となんでもありというか、concernを使う代わりにserviceに委譲するみたいな感じで、ドメイン層の泥臭い部分がここに押し付けられている、というのが実体に近いのかも知れません。
value_objects
value_objectsは読んで字のごとく前述した値オブジェクトのことです。同一性は持たないけれど比較ができる。例えばQiitaの投稿のタグ情報は、どのタグとそのバージョンの組で表されますが、これ自体には同一性は無いし、ライフサイクルもありません。
ActiveRecord
のcomposed_of
で紐付けるのもvalue_objectの1つです。
factories
これは複雑な集約を作るのに使うものです。一般的にはparameterを受け取ってmodelsを初期化します。
最後に
ところで今回挙げた参考文献のうち2つ(4と5)が @joker1007 氏によるものだという点は特筆すべきことだと思います。そして偶然にも(!)氏が執筆陣に名を連ねる パーフェクト Rails が技術評論社より近々発売されるそうです。
そして、より実践的なRailsアプリケーションにおいてモデルをどう扱っていくか、という事について相当ページを割いています。
とのことなので要チェックですね。中身読んでないのでこれは僕の希望的観測ですが、この記事で適当に書きなぐったことの上位互換な何かが書いてあるのだろうと思います。