設計の思想
GRASP(General Responsibility Assignment Software Patterns)は、オブジェクト指向設計における基本的な原則とパターンをまとめたものです。
そして、この考え方はドメイン駆動設計やDDD型の業務モデリングにおいて、必須の思想となっています。
クラスやそれ以上の粒度のモジュールに対して、どのように責任を割り当てるか?を決定する際に役立ちます。
GRASPは9つのパターンで構成されており、それぞれが特定の設計上の課題に対する解決策を提供します。
ドメインモデル図を完成させる、分析のフェーズでは、
下記の情報エキスパート と 高凝集 コントローラのみを考えればいいです。
そして、そのドメインモデルに対して、interfaceなどが登場する設計モデルの段階になってから、他の7つのパターンが必要となるという立ち位置です。
あくまでも、私は分析モデルと設計モデルを明確に分けていますが、
アジャイルモデリングの最小限のモデルという考え方に従うなら、
別にドメインモデルを分析と設計に分ける必要性は全くないです。
この記事の概要説明
『実践UML パターンによる統一プロセスガイド 第3版(Craig Larman 著)』を読んで、
GRASPとは何か、何がうれしいのかについて学んだことをまとめたものです。
前提条件
以下で述べる、ドメインモデルとは、業務上で扱われるデータと、
そのデータに対する処理を行う業務ロジックを1つにまとめた要素同士の関係図を指すことにする。
よって、以下のような属性リストのみが描かれたものは、
まだ業務ロジックが割り当てられていない概念モデルと呼ぶことによって、
明確に区別することにする。
情報エキスパート -Information Expert-
どんな考え方か?
ロジックは、そのロジックが必要とするデータのある場所に置きましょう
というカプセル化のために必要な考え方であり、次の項目の高凝集を満たすために必要なものである。詳しくは、下記の【情報エキスパート&高凝集】を参照ください。
目的
ドメインロジックの置き場所を間違うと、getter的な処理でデータを持ってくるなんていう
ドメイン貧血症状態なコードを誘発する。
ドメインモデリングを行う際にはマスト
GRASPは設計パターンではあるものの、
情報エキスパートと高凝集に関しては、ドメインモデリングの段階からすでに使っておかないといけないもの
である。
ドメインモデリング自体が業務フローと矛盾していたり、
ドメインロジックの置き場所が適当だったりすると、そのあとの設計フェーズ以降でカバーするのは容易ではない。
処理の割当先がない場合
基本、ドメインロジックの置き場所は必ずある。
ただし、自分たちの対象ドメインへの理解がまだ不十分であるときなどは、
エンティティ属性をすべて出すってことがまだできていないなんてことがざらにある。
そんな時には、いったん明示的に【ドメインサービス】という関心の層を
ドメインモデルとアプリケーション層との間に設けて、
そこの要素として仮のエンティティを用意し、そこに処理を割り当てておく。
そして、次のサイクルのドメインモデリングを行うフェーズで
ドメインの理解が向上し、そのドメインロジックが必要とする属性データが見つかった際に、ドメイン層に移動するリファクタリングを行う。
ただし、一戸注意点があって
このドメインサービスには、ドメインロジックの割当先に困ったときに、
仮のエンティティを定義する場所であり、それ以外の要素は置いてはならない。
という制約を設けないと、あっという間に様々な関心が詰め込まれた層になってしまうこと。
高凝集 -High Cohesion-
どんな考え方か?
クラスは一つの責務を全うするのに必要最低限かつ、その責務に特化しているプロパティとメソッドのみで構成されるべき という考え。
目的
仕様変更の際の変更箇所の特定にかかるコストを抑えたい。
そのためには、そのモジュールが唯一の目的を担う すなわち 単一責務の原則を満たしていてほしい。
すなわち、この高凝集
実現方法
高凝集には、まずはデータモデリング時の関連性の高いデータを集めるという
データの側面での高凝集の活動と、
そこに対して、情報エキスパートで処理を割り当てた上で、そのモジュール内部が密な状態にできる。
すなわち、カプセル化の完成のためには、データの高凝集+情報エキスパートが必要です。
詳しくは以下の記事をお読みください。
この考え方は、サービスレベルの設計においても基礎となります。
また高凝集によって、後続の疎結合も実現しやすくなる。
ちなみに、情報エキスパートと高凝集は、ドメインモデリングの作成中から意識できていないと、後工程の設計とかで大体自滅します。
そのためにも業務分析をアクティビティ図や状態図など、複数の観点からモデルで可視化することをお勧めします。
単一責任の原則との関係性
SOLID原則のSやコンポーネントの単一責任原則であるCCPは、この高凝集と密接に関係している。
単一責務とは、
同時に同じ目的で変更または再利用されるものを1つにまとめる
ということが尺度としてあるが、
そうなるためには関係性の強いデータと処理がまとまっていなくてはいけない。
ゆえに、単一責務を満たしたいのなら、まずは高凝集を目指すことがスタート。
また、一度満たした高凝集状態は仕様変更などの外的要因によって容易に崩れるため、
それが大きな技術的負債になる前に、定期的に高凝集状態に維持する活動が求められる。
それがリファクタリング活動の1つの具体的アクションである。
ポイント
高凝集にしたかどうかをカプセル化した後に必ずチェックする。
その際にオススメなのは、1つのモジュール内で
補助的にツールで検証する
個人的にはあくまでも補助的なものとして使うことを推奨している
LCOMという凝集度を測定してくれるものがある。
あくまでも補助的に使うことを忘れずにモデリングを行いましょう。
ただし、費用対効果合考えましょう。
優れたモデリングスキルと、他のチームとのチームトポロジー×モデリング体制
があれば、はっきり言って必要な場面はあまりないです。
具体的にどの程度の凝集度を目指すべきなのか?
結論から言うと、以下のレベル1または2を満たすようにモデリングしましょう。
業務部分のモデルである、ドメインモデルの時点で低い凝集度である時点で、
それ以降の設計フェーズなどで不要な結合度を下げることはできません。
一般的には、レベル1~3までがよい設計といわれるそうですが、
私個人は必ずレベル2までを目指すように推奨しています。
理由は単純、
少しでも怠けるとコードはすぐに汚くなるから
疎結合 -Low Coupling-
どんな考え方か?
モジュール間の関連性と依存性は、できる限り小さくするべきである
という考え。
関連性を少なくするとは各モジュールの公開メンバを少なくすることである。
詳細情報は以下の記事を参照ください。
目的
最大の理由は、結合点が少ないと
構造的にシンプルになるから影響範囲の特定がしやすくて嬉しい
ということ。
これによって、SOLID原則のOpen-Closedを満たせるし、疎結合ゆえに安心して変更がしやすくなる。
これは素早いデリバリーのために必須である。
実現方法
疎結合を満たすためには、上記の高凝集が必須である。
というか、疎結合をまずは目指すというよりも、高頻度で一緒に変更される場所を速やかに見つける。
他のパターンとの関係性
後述の間接化や変動からの保護などは、結果的に疎結合を実現するための手法である。
結合度合いを疎にして、変更の影響を最小限にしたい場合には、複数の方法を列挙してメリデメを比較してみよう。
クリエイター -Creator-
どんな考え方か?
インスタンスを生成する責任は、だれが持つべきか?というものである。
あるAというオブジェクトがあった際に、Aのコンポジション先になる者、Aのアグリゲーション先になる者、Aの生成を記録していく者、Aを密接に使用する者、Aの生成に必要な情報を持つ者などである。
GoFのデザインパターンの1つである、Factoryパターンがこれに該当します。
目的
オブジェクトの作成は、オブジェクト指向のシステムではあらゆるところで行われる活動である。
これをドメイン要素のコード中のあらゆる箇所に表現するのは可読性が低い。
生成者パターンは重要である。生成者パターンを有効に用いるシステムは、疎結合性が高くなり、理解のしやすさ、カプセル化が促進され、オブジェクトが今後再利用できる可能性が増加する。
実現方法
ドメインモデル内に生成の責務を担う要素が見当たらない場合には、
一旦アプリケーション層に新たな要素(純粋造形)を作成し、そこに生成系の責務を割り当てる。
もしも継続的なモデリングによって、ドメインモデル内に生成の責務を持つべきものが見つかれば、あとからそこへ生成系の処理を移動することも検討する。
代表例
デザインパターンの中では、Factoryパターンや、Builderパターンなどがこれに該当する。
また、生成系のコードは必ず書かなければならないものであるものの、
それをまじめに書いていると、全コードのうちの2/3を占めてしまうといわれている。
よって、フレームワーク側に生成系の責務を担わせてしまうことで、開発生産性を上げるのが一般的な手法である。
注意点
基本的に、生成系コードは業務の主な関心ではない場合に関しては、
ドメインの関心事ではなく、アプリケーションの関心事として扱うことをお勧めする。
(下図の赤い部分)
そのため、
ドメインモデル図上にはその処理を表現しない
という一定の規約を事前に定義しておいた方が、図のエンティティ部分のコードが複雑化せずに済みやすいという利点がある。
また、ヘッダと明細のようにライフサイクルを共にするケースにおいても、
モデルにコンポジションの関連線を結び、1つの集約の単位としてまとめておけば十分である。
わざわざヘッダ部分に生成の処理をドメインモデル上に表現せずとも、
一発で「ヘッダに生成の責務があるんだ」とわかりやすいからだ。
コントローラ -Controller-
どんな考え方か?
システムイベントの処理の流れの責任をどこに割り当てるべきかを定義するもの。
該当システムをカバーするコントローラが、該当システムで発生する全入力イベントを扱うべきという思想です。
目的
コントローラの目的は主に以下の3つです。
ユーザーインターフェースとビジネスロジックの分離
処理の一元化
再利用性の向上
これらをより深堀してみましょう。
目的の深堀
ドメインモデルの中に流れを制御する処理を含めないことによって、
外部操作に依存しない、変化に強い業務モデルを実現できるからです。
業務の順番に依存したモデルは、変化に強いとはいえません。
ドメインモデルとは、本来流れの順番に依存しないものだからです。
さらに、仮に処理の流れだけを後から変えたくなったという事態に陥っても
このコントローラーの処理だけを変えて、ドメインモデルはそのまま再利用という
ことが実現できます。
実現方法
ドメインモデル作成時
ドメインモデリングで責務を割り当てる際に、疑似的に外部との接点になる
コントローラーさんを用意します。
そいつに流れの制御をさせるようにしましょう。
それによって、シーケンス図を使って処理を割り当てる際に、余計な関心が混在するリスクを下げてくれます。
ビジネスとUIの関心を分ける
クリーンアーキテクチャの図においても、コントローラというものが出てくるように、
画面には流れを制御する責務を持たせないようにします。
そうすることによって、UI側とドメインモデルとのもろな結合を避けられます。
UIは本来、ユーザーとのインタラクションのみの責務にしたいものです。
そこに流れを制御する関心が混ざっていては、変更時のコストがかかってしまいます。
設計フェーズ
コンポーネント内の全体の処理の流れを制御するものを新たに定義する。(純粋造形)
Strategyパターンなどを実装する際にも、皆さんは処理を制御するコントローラーを用意し、
その属性として、interfaceを持つというコードを書いているでしょう。
(下図のContextのこと)
これもまさにこのコントローラーパターンです。
これ以外にも、Facadeパターンなど、皆さんが意識していないだけで、
様々な場所でコントローラは使われています。
代表例①
Facadeパターンや、マクロな粒度でいうとマイクロサービスのオーケストレーターがこれに該当する。
オーケストレーターさんのみが、全体のマクロな流れを制御する責務を持つことで、
他のマイクロサービスに対しては、流れの順番などの関心を混ぜなくて済む。
オーケストレーターパターンは、確かにオーケストレーターさんが単一障害点になりやすいというデメリットを持ってはいるものの、1か所にコントローラの責務をまとめられるという利点がある。
代表例②
Webアプリなどの場合、ユーザーからのHTTPリクエストをコントローラが受け取り、適切なビジネスロジッククラスに処理を委譲します。
補足
よくある例だと思うが、
クリエイターとこのコントローラーを1つにまとめてしまうという手法。
これはどっちが正解ということはない。
たしかにGRASPのパターンに忠実に従うとしたら、
クリエイターとコントローラーは分けることが推奨されるものの、
そこには変更頻度や再利用という観点は含まれていない。
よって、運用していく中で仮に対象としている
クリエイターとコントローラーが同じタイミングで変更されるとか、
必ずといっていいほど同時に再利用されるという場合には、
あえて1つにまとめておくことで、
「ああ、ここは必ず一緒に変更されるのね」というようにわかりやすくまとめておける。
この根底思想には、やはり以下のコンポーネントの凝集原則が用いられている。
間接化 -Indirection-
このパターンは、二つの要素の中間にオブジェクトを設け両者の仲介を行う責務を割り当てることで、二つの要素間の疎結合性(および再利用の可能性)を促進する。Model View Controllerにおいてコントローラがデータ(モデル)と表現(ビュー)の仲介を行うのはその一例と言える。
どんな時にもちいるのか
直接結合して、互いに影響を及ぼしあいたくない要素間に設けるもの。
具体例
代表的なものとして、デザインパターンのAdapterパターンがある。
ヘキサゴナルアーキテクチャとかで出てくるAdapter変換部分もこれである。
注意点
この間接化は、DBの関節窩テーブルのことではない。
必要とされる理由が全く異なるからだ。
また、この間接化を担う要素は、ドメインモデル内の要素ではない。
あくまでもアプリケーション層の関心であることに注意。
多態性 -Polymorphism-
つぎの
リスコフの置換原則のことを指している。
代表例
Strategyパターン、Policyパターン、Stateパターンなどがこれに該当する。
変動からの保護 -Protected Variations-
前の多態性と密接に関連している。
実現方法
変更リスクの高い箇所を明確にし、その箇所が変更されても、その影響が波紋しないようにinterfaceを作成しておく。
たとえば安定していてほしいはずのモノ系のモジュールが、
不安定な方針系のモジュールに入った変更の影響をもろに受けてほしくないとする。
こういった場合に、このinterfaceをかますことによって、安定していてほしい方へ変更が波紋しにくくなる。
結果的にこの例は、SOLIDのD【依存関係逆転の原則】を満たすようになっている。
注意点
変更が入るからと言って闇雲にinterfaceを定義して、変動から保護
純粋造形 -Pure Fabrication-
問題領域に登場する概念を表すクラスではなく、疎結合や、高凝集性、それらから得られる再利用可能性を実現するために作られるクラスである()。この種類のクラスはドメイン駆動設計ではサービスと呼ばれる。
目的
既存の出した概念に対して、情報エキスパートが実現できない場合、
無理に考えている処理を割り当てようとすると、単一責務を満たせなくなってしまいやすい。
それを回避するために、新しい要素を作り、そこに責務を割り当てることで、
様々な関心が混在しにくいようにすることが目的である。
実現方法
新しい名前の要素を作成し、上記のような
生成者、変動からの保護、コントローラの役割を担う者として定義する。
なので、実はかなりの割合で実は暗黙的にこのパターンを用いている。
注意点
作成した概念は、極力1つの責務を担うようにした方がいい。
どういうことかというと、生成の責務も間接化の責務も、コントローラの責務も担っている
というような状態は、あまり望ましくはないといえる。
ただし、それらのコードが仮に生成者・間接化・コントローラと分離されていても、
以下のような状況の場合には、1か所にあえてまとめてしまっていい。
常に変更されるタイミングが一緒
常に一緒に再利用される
これは、コンポーネントの凝集原則が背景にある。