GRASPパターンを原書で学ぶ
はじめに
本記事は『実践UML パターンによる統一プロセスガイド 第2版(Craig Larman 著、依田 光江 訳)』を読んで、GRASP原則とは何か、何がうれしいのかについて学んだことをまとめたものです。
GRASPとは?
General(汎用) Responsibility(責任) Assignment(割り当て) Software(ソフトウェア) Pattern(パターン)の頭文字を取ったもので、オブジェクト指向における責任割り当ての原則の9つのパターン。Craig Larmanによって提唱された。
情報エキスパートパターン
責任の遂行に必要な情報をもっているクラス(情報エキスパート)に
責任を割り当てるというパターン。
そもそも責任とは?
オブジェクトがどのように振る舞うべきかというオブジェクトの義務のこと。大きくは「実行責任」と「情報把握責任」に分けられる。
実行責任は、処理の実行やオブジェクトの生成などオブジェクト自体が何かを実行するものや、他のオブジェクトを制御したり何かを実行させたりするものなど、そのオブジェクトが○○というアクションを起こすべきであるという責任である。
情報把握責任は、データやオブジェクト間の関連などの情報をオブジェクトが把握していること、そのオブジェクトが○○という情報を持っているべきであるという責任である。
上記を踏まえると、「情報エキスパートパターン」とは、「情報を把握したりアクションを起こしたりするために必要な情報を一番知っているクラスに、情報把握やアクションを起こす仕事をしてもらおう」というパターンである。
何がうれしいの?
→オブジェクト間を疎結合にできる。
必要な情報を一番知っている「情報エキスパートな」クラスに仕事を任せる(責任を負わせる)ということは、後はそのクラスに、仕事をしてもらうために足りない情報を渡せばいいということ。
これを他の、「特にエキスパートではない」クラスに責任を負わせた場合どうなるか。情報エキスパートなクラスと比較して、仕事を遂行してもらうために外から与えなければならない情報が多くなる。ということは、オブジェクト間に余分な依存関係が増えてしまうということにつながる。
また、オブジェクトに負わせたい責任が何かによって、その責任を遂行するのに必要な情報を一番知っているクラスは変わる。そのため、情報エキスパートパターンに沿って責任を割り振ると、オブジェクトの振る舞いは必要な情報を持っているクラスに分散される。これにより関連性の強い機能や情報を局所化できるため、凝集度が上がることにもなる。
生成者パターン
- BがAオブジェクトを集約する
- BがAオブジェクトを含む
- BがAオブジェクトのインスタンスを記録する
- BがAオブジェクトを密接に使用する
-
Aの生成時にAに渡される初期化データをBが持っている
という条件のうち1つでも真である場合は、クラスAのインスタンスを生成する責任をクラスBに割り当てる
というパターン。
例を挙げると以下のようになる?
「BがAオブジェクトを集約する」
→例えばクラスBがAオブジェクトのコレクションである場合など。
「BがAオブジェクトを含む」
→例えばクラスBがAオブジェクトをフィールド・プロパティで持っている場合など。
「BがAオブジェクトのインスタンスを記録する」
→例えばクラスAがログのクラスで、クラスBがログを記録するクラスである場合
「BがAオブジェクトを密接に使用する」
→例えばクラスBのみがAオブジェクトを使用する場合。
「Aの生成時にAに渡される初期化データをBが持っている」
→例えばAのコンストラクタ引数に入れる値をクラスBが持っている場合。
何がうれしいの?
疎結合性を維持できる。 上で示した条件に当てはまるクラスAとクラスBの場合、そもそもクラスAとクラスBは関係性が密接である。生成する側のクラスBはクラスAを知っている(依存関係がある)ため、生成の責任を負わせても依存が増えない。
疎結合性パターン
**「結合性を疎に保つように責任を割り当てる」**パターン。
そもそも結合性とは?
1つの要素(クラスやシステム)が他の要素に対してどの程度依存しているかを表す尺度のことである。
結合度を高めてしまうと、クラス間の依存が多くなって修正時の影響範囲が大きくなったり、クラスを使用するためにより多くのクラスが必要になることでクラスの再利用が難しくなったりする。
クラスAがクラスBと結合している例を示す。
- AがBのインスタンスを持つ
- AがBのサービスを呼び出している
- AがBのサブクラスである
- AがBのインタフェースを実装している
次項で説明する凝集度と合わせてオブジェクト指向設計では常に考慮すべき基本的な設計原則である。
何がうれしいの?
クラスやシステムの保守性および再利用性が向上する。 他のクラスと疎結合なクラスは修正時の影響範囲が小さくなり、クラスの再利用に必要な他のクラスも少なくなる。
ただし、何でもかんでも結合性を低くすればよいということではない。例えばクラス間の結合を全くない状態にする場合、クラスはそのクラス自身ですべての仕事をすることになる。このとき、確かにクラス間の結合度は低くなるが、そのクラスの凝集性も低くなってしまう。
クラス間が結合することはむしろ当たり前のことであり、大切なのは 変わりやすく不安定なクラスとの結合度は低くしておくべきであるということ である。
高凝集性パターン
凝集性を高く維持できるように責任を割り当てるパターン。
そもそも凝集性とは?
要素(クラスやシステム)の責任がどの程度強く関係し、集束されているかを示す尺度のことである。例えば、全く異なる複数の分野の機能を一つのクラスが全て実装している場合には凝集性は低いといえるし、クラスの持つ責任の分野が一つに限定されていて、さらに分野内でも他のクラスと責任を分担してクラス同士が協調しながら仕事をする場合には、そのクラスの凝集性は高いといえる。
何がうれしいの?
クラスの保守・拡張性、および再利用性が向上する。 複数の異なる分野の機能を詰め込んだ凝集性の低いクラスは、クラスの持っているメソッド同士の関連性が低く理解が難しいうえに、それぞれの機能が色んな場所から呼び出されることになるため、修正時の影響範囲が大きくなる。クラスに負わせる責任の分野は一つにして、さらにさせたい仕事が大きい場合には複数クラスで分担させて責任の範囲を限定することで、影響範囲は小さくなり、拡張時にも機能の追加場所に迷わないで済むことになる。また、高凝集度のクラスは限定的な目的で使用できるため、再利用しやすくなる。
コントローラパターン
- システム全体、デバイス、またはサブシステムを表すクラス(「ファサードのコントローラ」)
- システムイベントが起きるユースケースシナリオを表すクラス(「ユースケースまたはセッションのコントローラ」)
上記のどれかを表すクラスへ、システムイベントメッセージを受け取る責任または処理する責任を割り当てる というパターン。
システムイベント?
外部アクターによって始動されるイベント。例えば「『ブザーを鳴らす』ボタンを押す」場合は、「『ブザーを鳴らす』ボタンを押す」という外部アクターの操作によって「ブザーを鳴らす」というシステムイベントが始動される。
コントローラ?
システムイベントを受け取る、もしくは処理する責任を持つオブジェクト。UIではなく、システムイベントと対応した操作のメソッドを定義する。例えば「ブザーを鳴らす」というシステムイベントがある場合、コントローラはそれに対応する「SoundBuzzer()」メソッドを持つ。
「ファサードのコントローラ」では、システムイベントを一つのコントローラで受け取ります。コントローラがアプリケーションの他レイヤの覆い(ファサード)になり、UIレイヤから他レイヤのサービスを呼び出す窓口になる。
「ユースケースのコントローラ」では、多くのシステムイベントが存在することでコントロールクラスが大きくなってしまう場合に、コントローラを複数用意する。このときコントローラの分割方針がシステムイベントのユースケースの分類となる。
何がうれしいの?
再利用性が向上する。 例えばウィンドウにアプリケーションロジックを書いてしまうと、そのロジックはウィンドウに結びついてしまうため、他のインタフェースで再利用することができなくなる。逆に、システムイベントに対応する操作をコントローラを用意してインタフェースから利用させるように設計すれば、アプリケーションロジックとインタフェースが切り離されるため、ロジックの差し替えや再利用が容易になる。
また、コントローラが一つの場合はアプリケーション全体のシステムイベントの状態を把握、管理することができる。
多相性パターン
選択肢や振る舞いが型によって変化する場合に、振る舞いを変化させる型に対して多相 的な操作を使ってその振る舞いのための責任を割り当てる というパターン。
多相性(多態性)とは?
実装の異なる同一の操作が複数のクラスに存在することで、メッセージに対して型ごとに異なる応答ができること。
例えば、同じ「ファイルをロードする」というメッセージに対して、入力されたファイルがテキストファイルならテキスト読み込みクラスの読み込みメソッドを使って読み込んで、JSONファイルならJSON読み込みクラスの読み込みメソッドを使って読み込み後デシリアライズして……というように、それぞれのクラスに「ファイルをロードする」メソッドを用意して別々の実装をすることで、型に応じてファイルロード時の振る舞いを変えることができる。
実現方法としては、抽象のスーパークラスに定義したメソッドを、複数のサブクラスでそれぞれメソッドを実装する方法が挙げられる。
何がうれしいの?
拡張が容易になる。 多相的な操作を使うことで、例えば「テキストファイルとJSONファイルだけじゃなくてCSVファイルも読み込みたい!」というように操作のバリエーションを増やしたい場合にも、新しくクラスを作って操作を実装することで簡単に叶えられる。また、条件判定文を書いて選択肢を判定させるわけでもないため、操作のバリエーションを増やしても操作を使用する他のクラスには影響しない。
純粋架空物パターン
高凝集性。疎結合性、再利用性を促進するために、問題領域の概念を一切表現しない人工的なまたは便宜上のクラスを作り、そこに高度に凝集した責任を割り当てるというパターン。
情報エキスパートパターンに沿ってクラスに任せる仕事を分担していった場合に、例えばDB操作や複雑な生成処理など、高凝集性と低結合性を保とうとするときに「情報エキスパートなクラスに責任を負わせるのが不適切である」仕事があることがある。このような場合に「DB操作する」「複雑な生成処理をやる」責任を負った新しいクラス(純粋架空物)を作成することで解決できる。
何がうれしいの?
高凝集性が維持できる。 「情報エキスパートなクラス」に余分な責任を負わせず、限定的なタスクに集中するクラス(純粋架空物)がその責任を引き取るため、どのクラスも高凝集性が保たれることになる。また、純粋架空物のクラスは情報エキスパートなクラスと依存しないため、再利用できる可能性も高くなる。
間接化パターン
他のコンポーネントまたはサービス間に介在する媒介オブジェクトに責任を割り当て、コンポーネントやサービスが直接的に結合されないようにする というパターン。
何がうれしいの?
コンポーネント間が疎結合に保たれる。 コンポーネント同士を直接依存させたくない場合に仲介役を導入することで、他のコンポーネントやサービスと切り離すことができる。なお、仲介役となるクラスは前項で説明した純粋架空物になることが多い。
バリエーション防護パターン
予測されるバリエーションまたは不安定箇所との接点を識別し、接点の周辺に安定したインタフェースが作成されるように責任を割り当てる というパターン。
システムが既存の複数の外部システムと関連を持つ必要があったり、将来的に増えるかもしれないシステムとも関連できるようにしなければならない場合に、システムがそういった「予測されるバリエーションまたは不安定箇所」に影響されないように守る必要がある。この場合、安定したインタフェースをシステムの接点の周辺に用意してシステム同士が間接的にしか関わらないようにすることで解決できる。
何がうれしいの?
システムが変化に強くなる。 バリエーション防護パターンのうれしさはSOLID原則のオープン・クローズドの原則と同様に、変更に対しては開いていて(バリエーションの拡張が容易で)、修正に対しては閉じている(影響範囲が小さい)ことである。
またインタフェースを介してシステム同士が関連するため、結合度も低くなる。
まとめ
およそ大半のパターンが「高凝集度」・「低結合度」がキーワードになっていた。GRASPパターンはつまり、何かオブジェクトにさせたい仕事があるときに、誰にその仕事を任せるべきかを考えるときの指針であって、可能な限り一つの分野の仕事に集中できるようにして(高凝集度)、かつ他のオブジェクトに依存せずに仕事を遂行できる(低結合度)ように設計する必要があると学んだ。
また、本記事ではあまり紹介できていないが、SOLID原則やGoFのデザインパターンと関連する部分もあり、合わせて学習するとよいと思われる。