『Layered Design for Ruby on Rails Applications』読書メモ
Kaigi on Railsで基調講演されたVladimirさんの書籍。
基調講演のスライドでも同じトピックの話を展開されてる。とてもよいのでこちらも必読。
問題設定
- デフォルトのRailsには抽象化レイヤーがMVCの3つしかないので、ある程度大きくなってくるとツラくなる
-
Fat Model, Thin Controller
でModelに処理を寄せることはある程度合意ができてきた - そうすると次はModelの責務が大きくなりすぎてくるので、それにどう対処していくか
という、実務規模でRailsアプリを運用したときにぶつかる課題に対して、
- Layered Architectureを意識しながら
- Rails Wayに沿った形で
- 責務が膨らんだ部分を抽象化レイヤーとして切り出していく
という方法で、ツラくないRails拡張を目指していく。
Layered Architecture
4層構造のレイヤードアーキテクチャ。
([DDD]ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か)
これだとドメイン層がインフラ層に依存しちゃってるので、そこを逆転させたオニオンとかクリーンアーキテクチャが推奨されることが多いけど、本書ではレイヤードアーキテクチャを採用。
Rails自体が実際こんな感じになってる気もするので、実態と合ってていいのかもしれない。
※「アプリケーション層」だと責務がふわっとするので、松岡さんの『ドメイン駆動設計 モデリング/実装ガイド』に従って以降は「ユースケース層」と読み替える。
RailsのMVCをLayered Architectureに当てはめると
書籍によると、こんな感じ。
(D. Vladimir『Layered Design for Ruby on Rails Applications』p.97)
ここで気になるのが、
- Controllerはユースケース層じゃないの??
- ユースケース層に対応するのがServiceクラスになってるけど、デフォルトのRailsではユースケース層が存在しないってこと??
というあたり。
Controllerはユースケース層じゃないの?
インフラ層は一旦置いておいて、RailsのMVCがレイヤードアーキテクチャの各層に対応してると考えるとすっきりするし、実際そう捉えている記事なども多そう。
でもこの本ではControllerはプレゼンテーション層になっている。
それはControllerの責務が、リクエストを受け取ってビジネスアクション(もしくは一連の手続き)に渡し、Viewをアップデートすることだから、とのこと。
The controller layer's responsibility is to translate web requests into business actions or operations and trigger UI updates.
(D. Vladimir『Layered Design for Ruby on Rails Applications』p.12)
これは確かにテストを書くと明らかで、Controllerがビジネスロジックを持ってしまっていると、実質ビジネスロジックをテストすることになり、色んなテストケースをカバーする必要が出てくる。
ただControllerはWebリクエストと密結合していて基本テストが書きにくいので、これはあんまりよくない。
なのでControllerはあくまでリクエストを受けてそれを誰かに渡し、結果をハンドリングするだけという、Scaffoldで作られるようなシンプルな形を理想形として考えることができる。
この場合ビジネスロジックのテストは別途それを担う部分で独立して書くので、Controllerのテストも非常にシンプルになる。
デフォルトのRailsではユースケース層が存在しないってこと??
MVCマッピングでの2つ目の疑問がこちら。
ユースケース層を担うのがServiceクラスしかないので、Serviceクラス使ってないデフォルトのRailsではユースケース層がないということになる。
これは実際どうなっているかというと、ControllerとModelがそれぞれ越境してユースケース層を担ってる(担ってしまっている)というのがよくあるケースなんじゃないかと。
Controllerによるユースケース層への越境
これはいわゆる Fat Controller
で、Controllerの中に手続的な処理を書いてしまっているケースが該当するのかなと。
Userのレコードを作成して、ユーザーに通知を送って、Subscriptionテーブルにもレコードを作る、みたいなことをControllerに書いちゃってるケース。
前提にもあったとおり、昨今ではこれを避けようという合意が取れてきているので、あんまりこういうことをしてるケースは少ないはず。
Modelによるユースケース層への越境
じゃあどうなるかというと、代わりにModelに処理が寄った結果、Modelがユースケース層も担ってしまっているケースがある。
典型的なのは悪名高いcallback処理で、さっきの例だとUserのafter_create
で通知送ったり関連レコードを作ったりするようなケース。
ドメイン知識なのかユースケースなのか(Modelが持つべき知識かどうか)の判断ポイントとして、callbackに条件がつくかどうかは大きい。条件がつくというのはユースケース依存してる可能性が高いので、それは本来Modelで負う責務ではないはず。
こういったModelの責務超過を、状況に応じて適切な抽象化レイヤーに切り出そうというのが本書のメイントピックになる。
(callbackのタイプを分類して、許容できるものと避けるべきものを定義してる箇所も非常におもしろい)
Serviceクラスですべて解決するのか
本書の定義に従うと、Serviceクラスがユースケース層を担うことになっている。図にするとこんな感じ。
Controllerがリクエストを受け付けて適切なServiceに渡し、Serviceは適宜ドメイン知識を持ったModelを呼んで処理を行い、結果をControllerに戻す。Controllerはその結果を持ってViewを出力する。
レイヤーの越境もなくなってきれいに整理できてるが、残念ながらServiceクラスの導入ですべて解決するわけではない。
Serviceは何でもできてしまうがゆえに、困ったらすべての処理がServiceに置かれるようになったり、本来ドメイン知識として持つべき内容も抱えてドメインモデル貧血症になってしまったりしがち。
なので本書の主張としては、
- 可能な限りServiceではなく、もっと責務を明確にした抽象化レイヤーに切り出す
- でもアプリケーションの要件は常に変わるので、一時置き場としてServiceが残ることは許容する
という感じになってる。
Serviceじゃなければどこに切り出すのか
本書で挙げられてる主な抽象化レイヤー:
- Query Object
- Form Object
- Filter Object
- Presenter/Decorator
- Policy Object
- Notification Layer
- View Component
もちろんこれ以外にもレイヤー抽出は可能で、基調講演のスライドにはWorkFlowオブジェクトなんかも出てる。
このあたりは以下の記事とも重なる部分が多そう。
抽象化レイヤーの設計指針
各レイヤーの具体的な話は本書を読んでもらうとして、共通する抽象化の手順・指針みたいなのがよかったのでメモ。
できるだけRailsWay
オブジェクトの命名など、できるだけRailsWayの規約に則る。
We want to come along with Rails, not fight against it.
ぼくらはRailsと喧嘩したいわけじゃない。Railsとうまくやっていきたいんだ
必ず基底クラスを挟む
前項とも関係して、たとえばPolicyレイヤーを作るなら ApplicationPolicy
を用意して各Policyはそれを継承する。他も同様に ApplicationForm
, ApplicationNotifier
などを用意する。
実際には各抽象化をやってくれる便利なgemを採用することも多いけど、直接使うのではなく基底クラスを挟むことで、インターフェースを統一できるようにする。
こうすることで、まず将来gemを変えることになったとしても、変更箇所を最小限にできる( Wrapperパターン
)。
また環境やデータによって振る舞いを変えたい場合は、Adapterパターン
を採用して簡単に切り替えられるようにすることもできる。
メソッド名をカラフルにする
Serviceオブジェクトのベストプラクティスとして、.call
だけ公開インターフェースとして用意するcallable
オブジェクトにすることが挙げられる。
Serviceを具体的な別オブジェクトに切り出していくにあたって、そちらもcallableオブジェクトなんだけど、役割に応じてもうちょっとカラフルな名前にすることが提案されている。
- Queryオブジェクト:
XxxQuery.resolve
- Formオブジェクト:
XxxForm.save
(これはActiveModelと合わせて) - Filterオブジェクト:
XxxFilter.filter
みたいな感じ。
gemで.call
メソッドで定義されてる場合でもエイリアスを当てていて、このあたりはWrapperパターンがうまく効いていてよさげ。
違う考え方:ユースケース層をわけなくてもいいんじゃないか
37signalsの「素のRailsは十分に豊かである」で紹介されているアプローチは、本書の考え方とはちょっと違う。
私たちは、これらのモデルや公開するAPIの設計方法については細心の注意を払いますが、それらへのアクセスをオーケストレーションする追加の層についてはほとんど価値を見出していません。
言い換えれば、私たちはコントローラのアクションを実装するのにデフォルトではServiceもアクションもCommandもInteractorも作成しません。
このアプローチも当然Modelに処理が集まることになるけど、それには以下2つの戦術で対応しているとのこと。
- モデルのコードをconcernsで整理する
- 機能をオブジェクトの追加システムに委譲する(純粋なオブジェクト指向プログラミングとも呼ばれます)
ユースケース層(アプリケーション層)を分離しないことについては、EvansのDDD本も引用した上で問題ないというスタンス。「ドメイン層」を他とわけることがとにかく大事で、他の層についてはそこまで厳密じゃなくてもよいと。
オリジナルのDDD本では、DDDを可能にするのは「ドメイン層の決定的な分離」であると述べつつ、「プロジェクトによってはユーザーインターフェイスとアプリケーション層を厳密に区別しないこともある」とも述べています。