この文章は祈りです。 主にRuby on Railsアプリケーションを想定した話です。
Ruby on Railsアプリケーションでは、Fat Model問題という問題が起きることがあります。 ドメインオブジェクトが肥大化しメンテナンスしにくくなる問題です。 Fat Model問題に対応するためにサービスレイヤーを導入することがあります。 「ドメインモデル貧血症」と呼ばれているアンチパターンです。
ドメインのロジックをドメインオブジェクトの中に入れないという設計ルールに従っているのでしょう。その代わり、すべてのドメインロジックを含むサービスオブジェクト群が存在しているのです。
Fat Modelを恐れよ
Fat Modelは「単一責任原則」を満たしていないモデルです。
1つのサブシステムやモジュール、クラス、関数などに、変更する理由が2つ以上あるようではいけない
重要なのは「変更する理由が2つあるということは、機能変更のためにあるクラスを変更すると別の機能が壊れる可能性がある」です*1。 Fat Modelの問題は行数の多さではありません。 複数の責務を持つため、あるドメインオブジェクトへの変更が、変更したくない機能にも影響を与える可能性があることです。
Fat Modelがあるとアプリケーションの変更が難しくなります。
地獄への道は善意で舗装されている
サービスを求める
どうすればFat Model問題を解消できるでしょうか? モデルが複数の責務を持っているのが問題です。 責務をモデルの外に出してしまいましょう。 各責務を担当するサービスオブジェクトを定義し、ロジックをサービスオブジェクトに移動します。 1つのサービスオブジェクトは1つの責務を持ち、単一責任原則を満たします。 すべてのFat Modelはなくなります。
レイヤーを得る
人間はサービスオブジェクトを見るとレイヤーだと認識します。 なぜかはわかりません。 サービスオブジェクトの群れとドメインオブジェクトの群れを別のものだと認識し、そこに境界を求めるのかもしれません。 サービスオブジェクトが、コントローラーとドメインオブジェクトに挟まれているのがレイヤーと思わせるのかもしれません。 レイヤーだと認識した人類は「レイヤーは薄くあるべき」と考えます。 サービスオブジェクトからサービスオブジェクトを呼び出すことを避けはじめます。 つまりサービスオブジェクトの処理を他のサービスオブジェクトに「委譲」しなくなります。
抽象を失う
「委譲」しなくなった結果、サービスオブジェクト間の共通の処理はどうやって共通化するでしょうか? 「継承」です。 Ruby on Railsの場合はConcernを使って処理を共通化します。 ConcernはRubyのMix-inという多重継承のための仕組みをRuby on Railsアプリケーションで使いやすくしたものです。 オブジェクト間にis-a関係が無いのに実装上の都合で継承を使います。 人間の認知における抽象-具象の方向と継承の方向が合わなくなります。 複数のサービスオブジェクトを抽象的なサービスオブジェクトとして認識できなくなります。
オブジェクトは去る
レイヤーの認識はコントローラーとの近さからサービスオブジェクトをデータより遠く手続きに近いものだと認識します。 そしてサービスオブジェクトに「○○Service」という名前を与えます。 ひとたび「○○Service」という名前を得れば、それは手続きです。 サービス業が「もの」を売らないように、サービスが「もの」を持つはずがありません。 「サービスオブジェクトは手続きをもつ、ドメインオブジェクトが状態を持つ」ルールを導入します。 データと振る舞いを分割してします。 active recordパターンを使ってオブジェクト指向でモデリング出来るようにしたのに、利点を捨てました。
具象の林 現る
「サービスオブジェクトは手続き」だと認識した人類は、「サービスオブジェクトはただ一つの静的なパブリックメソッドを持つ」ルールを導入します。 サービスオブジェクトは「もの」ではなく「行為」になります。 これがイベントとしてモデリングしてデータベースにしまえるものなら良かったのですが・・・そうではない処理の羅列が生まれます。 サービスオブジェクトに適切な名前を与えて、を抽象化して認識することを難しくします。 あるサービスオブジェクトが何をするオブジェクトなのかを認識するには、ソースコードを読んで処理を追いかける必要があります。 ひたすら具象と向き合います。
林は深く
サービスオブジェクトが具象としてしか認識出来ないのであれば、2つのサービスオブジェクトの共通はどうしたらわかるでしょうか? 一行一行比較していけばわかります。 10あるサービスオブジェクトが共通の処理を持つかどうか探し出せるでしょうか? できないのです。 新しいサービスオブジェクトを追加するときに、既存のサービスオブジェクトが使えるのかどうかわかりません。 このとき大変面白いのは、読みにくいソースコードほどコピペされます。 機能をもとに似たサービスオブジェクトを探します。 そこに読みやすいソースコードが書かれていれば共通化の努力をするでしょう。 読みにくいソースコードが合ったらどうするでしょうか? コピペしてわかるところだけ修正します。
データを覆い隠す
あるサービスオブジェクトがリレーショナルデータベースのどのテーブルを操作しているかわかるでしょうか? 処理を追いかけて、どのドメインオブジェクトを参照しているか確認します。 さらに参照してるドメインオブジェクトのうちどれが参照のみでどれが変更しているか分類していく必要があります。
さて、リレーショナルデータベースのテーブルを変更したくなりました。 どのサービスオブジェクトに影響がでるかわかりますか?
Fat Modelを恐れドメインオブジェクトの責務を「機能」と認識した結果人類は、複数のテーブルに責務をもつサービスオブジェクトを作り出してしまいた。 そこはサービスオブジェクトの群れるサービスレイヤーという新たな地獄でした。
人はどうすれば救われるのでしょうか?
天国は善行で満ちている
実は「単一責任原則」を満たしていなくても、1クラスに2つくらい責務があっても、変更前に影響範囲を調べればなんとかなります。 そうはいってもアプリケーションに機能を追加していくと、クラスの責務は増えていきます。
どういうわけか特定のクラスだけ、責務が増えていく傾向があります。 世の中で挙げられている例では、UserクラスやEmployeeクラスが多いです。 実際のアプリケーションのFat Model問題では、ほとんどのドメインオブジェクトはFatになりません。 特定のドメインオブジェクトだけが肥えます。
Fat Model問題に必要な対策は、Fat Modelになっているドメインオブジェクトを特定し、分割することです。 分割には「委譲」を使います。 Fat ドメインオブジェクトのソースコードをにらみつけます。 すると、小さなデータと振る舞いの塊がうごめいているのが見つかります。
消えていなくならないうちに、さっと捕まえます。 そいつを新しいドメインオブジェクトとして抽出します。 「あとでやろう」と考えたら、Fatドメインオブジェクトの中に溶けていき、消えます。
Ruby on Railsでは、ドメインオブジェクトはリレーショナルデータベースのテーブルと対応していることが多いです。 ドメインオブジェクトからデータを分割しようとしたら、テーブルの分割が必要になります。 ここは歯を食いしばってマイグレーションします。 たいていはデータのマイグレーションも必要です。歯を食いしばってマイグレーションします。
こう書くと、Ruby on Railsがドメインオブジェクトとリレーショナルデータベースのテーブルを対応させているから、サービスレイヤーが生まれるように思えるかもしれません。 2003年に「ドメインモデル貧血症」はすでに存在しています。 Ruby on Railsは2004年生まれです。
参考
- 単一責任の原則(Single responsibility principle)について、もう一度考える | オブジェクトの広場
- 【SOLID原則】単一責任の原則 - SRP
- 単一責任原則で無責任な多目的クラスを爆殺する - Qiita
- よくわかるSOLID原則1: S(単一責任の原則)|erukiti|note
- コードの臭い - Wikipedia
- 委譲 - Wikipedia
- 継承のことは忘れよう - オブジェクト指向プログラミングを極める
*1:「単一責任原則」は良い設計の指標と考えても理解できませんでした。満たして居ないことが「コードの臭い」となる、と理解しました。