「アジャイルソフトウェア開発の奥義」を読んで第二弾。オブジェクト指向設計の原則に関するメモです。自分で読んで思い出せるくらいの内容しかメモってないと思われるので、もっと詳しい解説が欲しければ本書を買ってください。
本書には、クラス設計の原則として5つの原則が載っています。
- 単一責任の原則 (The Single Responsibility Principle: SRP)
- オープン・クローズドの原則 (The Open-ClosedPrinciple: OCP)
- Liskovの置換原則 (The Liskov Substitution Principle: LSP)
- 依存関係逆転の原則 (The Dependency Inversion Principle: DIP)
- インターフェース分離の原則 (The Interface Segregation Principle: ISP)
パッケージ設計の原則として次6つの原則が載っています。
- 再利用・リリース等価の原則 (Resuse-Release Equivalency Principle: REP)
- 全再利用の原則 (Common Resue Principle: CRP)
- 閉鎖性共通の原則 (Common Closure Principle: CCP)
- 非循環依存関係の原則 (Acyclic Dependencies Principle: ADP)
- 安定依存の原則 (Stable Dependencies Principle: SDP)
- 安定度・抽象度等価の原則(Stable Abstractions Principle: SAP)
『プログラマのためのJava設計ベストプラクティス』という本にも、オブジェクト指向設計の原則に関しての解説があります。
設計における原則
オブジェクト指向設計におけるデザインパターンよりも上の概念に、原則というものがあります。原則は主に5つか6つあります。本書では5つの原則に関して、わかりやすい説明と理解しやすい例を挙げて説明してくれました。
本書は、プログラム設計者だけでなくプログラマの人にも、是非読んでもらいたいです。だてに「Jolt Award受賞」はしていません。「アジャイル開発はいいぞー」なんて偏った解説本ではなく、設計・テスト・原則・パターン・プラクティスといった、ソフトウェア開発における重要な側面を、丁寧かつわかりやすく説明してくれています。ほとんどの解説に目からウロコがおちます。最高の本ですので、一度立ち読みでもしてみてください。
クラス設計の原則
- 単一責任の原則(The Single Responsibility Principle: SRP)
- オープン・クローズドの原則(The Open-Closed Principle: OCP)
- Liskovの置換原則(The Liskov Substituion Principle: LSP)
- 依存関係逆転の原則(The Dependency Inversion Principle: DIP)
- インターフェース分離の原則(The Interface Segregation Principle: ISP)
パッケージ設計の原則
- 再利用・リリース等価の原則(Reuse-Release Equivalency Principle: REP)
- 全再利用の原則(Common Reuse Principle: CRP)
- 閉鎖性共通の原則(Common Closure Principle: CCP)
- 非循環依存関係の原則(Acyclic Dependencies Principle: ADP)
- 安定依存の原則(Stable Dependencies Principle: SDP)
- 安定度・抽象度等価の原則(Stable Absstractions Principle: SAP)
クラス設計の原則
単一責任の原則(The Single Responsibility Principle: SRP)
クラスを変更する理由は一つ以上存在してはならない
クラスには一つの役割だけを持たせるべきです。単一責務のクラスは、クラス自体の変更理由がたった一つに絞られます。その責務が変更された場合だけクラスの変更がおきます。
- Tips
- 複数の役割を持ったクラスは、変更理由も複数になっていまい、変更部分がわかりづらくなります。
単一責任の原則(SRP)では「役割(責任) = 変更理由」として定義されています。役割の観点からクラスの設計を行うと、複数の役割を負っているかの判断がつくにくい場合があります。そんなときは、アプリケーションが今後、どんな変更をされるかを考えてみるといいです。
本書の例に、Modem というクラスがでてきました。Modem クラスは、「接続の管理」と「データ通信」の2つの役割を持っています。アプリケーションで、「接続の管理」と「データ通信」が別々に変更されうるのならば、Modem クラスは、2つの役割を持っていることになります。しかし、常に同時に変更される場合は、1つの役割を持っていると言っても問題ありません。
このように、クラスの役割を見極めるのに、変更理由の観点からクラスを眺めるのも一つの手です。
- Tips
- 変更の理由が変更の理由たるのは、実際に変更の理由が生じた場合だけである
単一責任の原則を適用するには、どのような点に気をつければいいのでしょうか?その答えの一つに、「GRASP (General Responsibility Assignment Software Patterns)」というものがあります。詳しくは『実践UML』を参考にするといいと思います。ここでは簡単にまとめておきます。
GRASP
GRASP には基本パターンが5つと、追加パターンが4つ(以上)あります。GRASP というのは、クラスの責務割り当てにおける一般原則のパターンのことです。ここでいう「責務」とは、次の2つのことを言います。
- 情報把握 (knowing)
- 実行 (doing)
情報把握責任の例には、カプセル化しているデータを把握していることや関係しているオブジェクトを把握しているといったものがあります。また、実行責任の例には、自分自身で何かを行うことや関連するオブジェクトのアクションを起動させることなどがあります。
GRASP の基本パターンには次のものがあります。
- Expert (エキスパート)
- Creator (生成者)
- High Cohesion (高凝集性)
- Low Coupling (疎結合性)
- Controller (コントローラ)
- Expert (エキスパート)
- 責務の遂行に必要な情報を持っているクラスに、責務を割り当てるものです。
- Creator (責任者)
- 他のクラスのインスタンスを生成する責務を割り当てるパターンです。クラスの関係が「集約」や「コンポジション」になっている場合や、密接にかかわりを持つクラス、インスタンスの初期化データを持つ場合に、責務を割り当てます。
- High Cohesion (高凝集性)
- 機能の類似性が高まるようにクラスに責務を割り当てるパターンです。ただし、あまりに多い仕事量を持つクラスでは、凝集性は高いとはいえなくなります。
- Low Coupling (疎結合性)
- クラス間の関連を出来るだけ少なくなるように責務を割り当てるパターンです。ただし、再利用性を重視しない場合は、特別重要にならない場合があります。
- Controller (コントローラ)
- システムイベントを処理する責務をコントローラと呼ばれる「システム全体」を表したようなクラスに割り当てるパターンです。ユースケース単位で作成されるのが普通です(Facadeパターン GoF)。
オープン・クローズドの原則(The Open-Closed Principle: OCP)
ソフトウェアの構成要素(クラス・モジュール・関数など)は拡張に対して開いて(オープン: Open)いて修正に対して閉じて(クローズド: Closed)いなければならない。
OCPを上手く適用してあるシステムは、変更に対してコードの追加という手段で対処できるようになります。既存のコードに手を加えなる必要がなくなるため、動いているコードを壊す恐れがなくなります。
- 拡張に対して開かれている(オープン: Open)
- モジュールの振る舞いを拡張できるということ。仕様変更が起こった場合に、モジュールの振る舞いを追加することで対応できる。
- 修正に対して閉じている(クローズド: Closed)
- モジュールの振る舞いを変更しても、既存のソースコードやバイナリコードは影響を受けない。
- Tips
- オープン・クローズドの原則の鍵は、「抽象」にあります。モジュールをある固定した「抽象」に依存させておけば、修正に対してコードを閉じることができるようになります。「抽象」を使えば、派生クラスを新たに追加するだけで、振る舞いを拡張できます。
本書を読んでいて、ウロコが落ちたのが下の図です。
この図は、Client はClient Interface という抽象を利用して処理が組まれていて、Client Interface の実装が Server によって提供されるというものです。Client が依存する抽象の名前が、Client Interface であるのがポイントです。
「抽象クラスはそれを実際に実装するクラスとの関連よりも、それを利用するクラスとの関係のほうがずっと密接」という事実があるため、インターフェースには、Client Interface という名前がついています。この辺の詳しい説明は、本書を読んでください。
- Tips
- オープン・クローズドの原則に順ずるためにもっとも典型的に使われるのが、「Template Method パターン」です。
いつ「抽象」を導入すればいいのか
これには、2通りの答えがあります。一つは、設計する人がどういった種類の変更に対して自分の設計を閉じたいのかを選択し、その変更に対して閉じるという、先を見越した対策。 もう一つは、実際に変更が起きた場合に「抽象」を組み込むという対応。すべての変更に対してこの原則を適用するにはコストがかかりすぎるため、実際に変更されるまで、この原則を導入しないという方法もあります。
とはいっても、運用後の変更で、「抽象」を導入するとバグを生み出してしまう可能性も無くはありません。ここで大活躍するのが「テストファースト」です。確実にテストを行えるようにするために設計しておけば、テストの変更で取り入れた「抽象」の多くが、実際の運用時の変更に耐えられるものになっている場合が多いのです。
- Tips
- 早まった抽象をしないことも、抽象を使うのと同様に重要なこと
Liskovの置換原則(The Liskov Substituion Principle: LSP)
派生型はその基本型と置換可能でなければならない
派生型に求められるのは、「基本型の能力+アルファ」であるということ。基本型にできることが、派生型でできなくなっているような継承の仕方では、LSP に反しているといえます。
これは、先ほどの OCP にあった、抽象を使って実装と切り離すということができなくなる事を示しています。抽象の変わりに実装を使った場合の振る舞いが予期できないものになってしまうからです。
例えば、抽象クラスで宣言された get というメソッドを、実装時に使わせたくないなどの理由で例外が発生するようにしてしまった場合、実装クラスを抽象クラスの変わりに使えなくなってしまう。if 文の分岐や instanceof などを使って、オブジェクトの型を判定しなければならなくなってしまいます。
契約による設計
これは、契約によってクラスを使うクライアントが必要としている振る舞いを、クラスの作成者に強制させることができるというものです。これには、事前条件 と 事後条件 というものをつかいます。
ここで、ポイントなのは、「どうしてクラスのクライアント側が、機能の実装者に契約を課すことができるのか?」という点です。実際のコーディングでは、機能の実装者が使用者に契約を課すことはできても、使用者が実装者に契約を課すことは明示的にするのはむずかしいです。
でも実は一つだけクライアントが実装者に契約を課す方法があるのです。OCP のところで出てきた図をもう一度見ると答えがでてくるのではないでしょうか? そう!Client に、必要なインターフェースを定義してもらうのです。クラスの使用者である Client は、自分の定義したインターフェースにのみ依存します。 Server はそのインターフェースを実装するときに、契約に沿って実装すればよいのです。
この、「契約による設計」を適用することで、Liskov の置換原則を守ることができるようになります。この辺の解説が非常に面白いので、一度本書を読んでみてください。
参考
依存関係逆転の原則(The Dependency Inversion Principle: DIP)
- 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである
- 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである
上記の図は、上位モジュールである Policy 層が下位モジュールの Service 層や Utility 層に依存してしまっています。この依存関係を反転させたのが下記の図です。
上位のモジュールは、下位のモジュールに依存しなくなっています。注目なのは、下位のモジュールが、上位モジュールと同レベルの「抽象」に依存した点です。
抽象に依存せよ
この原則の本質は、「プログラムは具体的なクラスに依存してはいけない。プログラム内の関係はすべて、抽象クラスかインターフェースで終結すべきである。」という点にあります。この辺り、さらに上位のパッケージやモジュールといった集合に関しても同様の議論があります。本書の後半に、パッケージの結合度のところで、安定度に関しての説明時にさらに鮮明に言いたいことがわかるようになると思います。
- Tips
- プログラムの関係は、抽象クラスかインターフェースにのみ依存するようにする。疎結合であることが、よいプログラムのひとつの指針です。
インターフェースが変更されるのは、クライアントが変更を必要としたときのみ
抽象インターフェースはクライアントクラスが宣言するものであり、それはクライアント自らが必要なサービスを受けるためだという視野に立つと、インターフェースが変更されるのは、クライアントが変更を必要とするときだけです。
インターフェースは、誰の所有物でもありません。インターフェースはたくさんのクライアントによって利用され、多数のサーバーに実装されることになるので、どのグループにも所属しない独立した存在でなければならりません。
インターフェース分離の原則(The Interface Segregation Principle: ISP)
すべてのインターフェースを一つのクラスに押し込めてしまうのではなく、関連性を持ったインターフェースはグループ化し、抽象基本クラスとして分けて利用すべき
クラスを利用するクライアントが違うのならば、インターフェースを別にしておくべきです。クライアントは、利用するインターフェースに依存することになるからです。これは、依存関係逆転の原則でもあったように、インターフェースの変更は、クライアントが変更を要する場合にとどめておくべきです。
- Tips
- クラスのクライアントが違うのなら、インターフェースは別にしておくこと。
クライアントが利用するインターフェースごとに分離させておけば、変更時にクライアントに影響する度合いが少なくなります。たくさんのインターフェースを実装しているクラスに変更が入った場合、クライアントに、自分の利用しないインターフェースの変更による影響を与えてはいけないのです。
- Tips
- インターフェースのクライアントに、クライアントが依存しないメソッドへの依存を強制してはならない。
パッケージ設計の原則
Java言語で開発を行っていると、パッケージという言葉を聞くと思います。UML にも「パッケージ」というものが存在します。Java言語のパッケージとほとんど同じ意味で使われる、「機能のグループ単位」です。パッケージはサブシステムとも呼ばれます。
パッケージ分けの指針として、マーチン・ファウラーの著 『UML モデリングのエッセンス 第2版』 ではこんなことを言っています。
クラスの場合それをグループにまとめるための何らかの方針がなければ、グループ化は意味のない単なるまとまりになってしまいます。私が最も効果的だと感じ、UMLにおいても最も強調しているのは、依存関係に基づくグループ化です。
- Notice
- クラスと、パッケージの一番の大きな違いは、「パッケージは依存関係に推移性をもたない」ということです。
再利用・リリース等価の原則(Reuse-Release Equivalency Principle: REP)
再利用の単位とリリースの単位は等価になる
この原則は、「再利用の単位(パッケージ)はリリースの単位より小さくなることはない」ということです。裏を返せば、「リリースの単位は、パッケージごとに行う」ということです。パッケージという言葉を、Java であれば「Jar ファイル」に置き換えると分かりやすいと思います。バグの修正やバージョンアップに伴うリリースは、クラス単位ではなく、Jar ファイルの単位で行うということです。
この原則は、パッケージのあり方を考えさせてくれます。
パッケージに含まれるクラスは、すべてが再利用されるか、すべてが再利用できないかのどちらかにすべきだ。
とあるように、再利用を目的としたパッケージには、再利用できないパッケージは含めるべきではありません。例えば、「Jakarta Commons」のライブラリパッケージ(再利用可能)にドメイン固有のクラス(再利用不可能)を含めるべきでないということです。
全再利用の原則(Common Reuse Principle: CRP)
パッケージに含まれるクラスは、すべて一緒に再利用される。つまり、パッケージに含まれるいずれかのクラスを再利用するということは、その他のクラスのすべてを再利用するすることを意味する。
「同じパッケージに含めるクラスは、一緒に使われる傾向にある」ということを意味しています。例えば、List インターフェースと ArrayList クラスは一緒に使われる場合が非常に多いと思います。こういうものは、一緒のパッケージに入れておいたほうがよいのです。また、逆に、FileReader クラスとPreparedStatement クラスは、たぶん一緒には使わないでしょう。
- Tips
- 互いに強い関連性を持たないクラスを同じパッケージにまとめるべきではない
閉鎖性共通の原則(Common Closure Principle: CCP)
パッケージに含まれるクラスは、みな同じ種類の変更に対して閉じているべきである。パッケージに影響する変更はパッケージ内のすべてのクラスに影響を及ぼすが、他のパッケージには影響しない
全再利用の原則は、互いに強い関連性を持つクラスは同一パッケージにするべしと言っています。強い関連性を持ったクラスというのは、同じ変更理由で修正しなければならない場合が多いので、全再利用の原則と一緒に考えるといいと思います。
非循環依存関係の原則(Acyclic Dependencies Principle: ADP)
パッケージ依存グラフに循環を持ち込んではならない。
クラスの依存とパッケージの依存の違いは「パッケージの依存には、推移的な依存関係はない」という点だと『インターフェース分離の原則を適用したシステムであれば、パッケージ間は抽象にのみ依存するように出来ると思います。
- Tips
- パッケージ依存サイクルに、循環を持たせてはいけない。
循環を断つ方法
パッケージ依存循環を断ち切るためには、依存関係逆転の法則 を使います。依存しているクラスへのインターフェースを自分のパッケージに作る(移動させる)ことで、依存関係を逆転できます。
安定依存の原則(Stable Dependencies Principle: SDP)
安定する方向に依存せよ。
変更することを意識して作られた(不安定な)パッケージが、変更しにくい(安定している)パッケージに依存されてはいけないというのが、この原則です。
「安定している」とは、変更しづらいという意味です。変更しづらいというのは「他のクラスやパッケージから依存されている場合です。つまり、依存されればされるほど変更が難しくなるのです。逆に「不安定」とは、依存ばかりしているクラスやパッケージのことで、依存先が変更されると自分も変更しなければなりません。
安定した方向に依存せよとはつまり、抽象に依存せよということです。システムの上位レベルの設計(アーキテクチャやフレームワーク)は、安定したパッケージに配置されるべきものです。ただ、アーキテクチャやフレームワークに柔軟性を持たせたい場合もあります。例えばプラグインを付け加えたりしたい場合です。そういったときにはオープン・クローズドの原則 が答えを握っています。
実装に依存するな。抽象に依存せよ。
安定度・抽象度等価の原則(Stable Absstractions Principle: SAP)
パッケージの抽象度と安定度は同程度でなければならない。
安定度の高いパッケージは、抽象度が高くなければならないと言っています。逆に、不安定なパッケージは具体的でなければならないということです。これは、抽象度が高いクラス(インターフェース)は、変更が少ないということに起伏しています。安定したパッケージに出来るだけ変更を発生させないためには抽象的である必要があります。
参考
- この記事の元ネタです。オブジェクト指向設計を行うなら、本書を一度は読んでおくべし。
- この本にもオブジェクト指向設計の原則に関しての解説があります。
- GRASP の原則が載っています。UMLを使ったオブジェクト指向設計が良くわかります。