COLUMN

第2回【IT技術系コラム】オブジェクト指向設計原則-(1)クラス設計に関する原則

オブジェクト指向設計原則-(1)クラス設計に関する原則

前回はオブジェクト指向言語の3つの特徴について述べさせていただきました。

  • カプセル化(Encapsulation)
  • 継承(Inheritance)
  • 多態性(Polymorphism)
上記の特徴は一般的なオブジェクト指向言語の初学書においても触れられていることが多いのでご存じの方も多かったかもしれません。 そこで、今回以降3回にわたってもう一歩踏み込み、「オブジェクト指向設計原則」について触れさせていただきます。 今回はこのうち、「クラス設計に関する原則」を取り上げます。

11のオブジェクト指向設計原則

オブジェクト指向設計原則は、オブジェクト指向言語を用いた設計・開発を行う際に有用となる概念原則集であり、 Robert C. Martin氏、Bertrand Meyer氏、Barbara Liskov氏等を中心とする様々なコンピュータ・サイエンティストによって考え出されたアイデアを、 Robert C. Martin氏が取りまとめたものとされています。

Robert C. Martin氏が設立したObject Mentor社(http://www.objectmentor.com/)において、これらのアイデアの原文が公開されていますので、ご興味のある方はご一読ください。 (Resources→Published Articlesページ
また、書籍(翻訳和書)として下記が出版されており、まとめられています。

このオブジェクト指向設計原則では、クラス設計に関する原則、パッケージ凝集度に関する原則、パッケージ結合度に関する原則の3種類に分類・整理しています。

  1. 5つのクラス設計に関する原則
    1. 単一責任の原則(SRP:The Single Responsibility Principle)
    2. オープン・クローズドの原則(OCP:The Open Closed Principle)
    3. リスコフの置換原則(LSP:The Liskov Substitution Principle)
    4. 依存関係逆転の原則(DIP:The Dependency Inversion Principle)
    5. インタフェース分離の原則(ISP:The Interface Segregation Principle)
  2. 3つのパッケージ凝縮度に関する原則
    1. 再利用・リリース等価の原則(REP:The Reuse/Release Equivalence Principle)
    2. 閉鎖性共通の原則(CCP:The Common Closure Principle)
    3. 全再利用の原則(CRP:The Common Reuse Principle)
  3. 3つのパッケージ結合度に関する原則
    1. 非循環依存関係の原則(ADP: The Acyclic Dependencies Principle)
    2. 安定依存の原則(SDP: The Stable Dependencies Principle)
    3. 安定度・抽象度等価の原則(SAP: The Stable Abstractions Principle)

今回はこのうち、「クラス設計に関する原則」を取り上げます。

1.a 単一責任の原則(SRP)
THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.
クラスを変更する理由が複数存在してはならない。

オブジェクト指向におけるオブジェクトの雛型となるクラスは、データと処理の集合としてカプセル化されています。 ここで、この「単一責任の原則」が守られていないクラスは、経験的に非常に脆弱で、再利用しづらいクラスであるといえるでしょう。 不幸なことに我々は、役割(メソッドと読み替えてもよいかもしれません)が何でもかんでもつめこまれたクラスというのを、一度は見たことがあるのではないでしょうか? 万が一発見したならば、速やかにクラスが単一の理由によってのみ変更されるように(つまり、単一の役割、責任を持つように)単純化整理(リファクタリング)するのが賢明です。 放置すると後にいわゆる「スパゲッティ状態」となり、非常に保守性の低いシステムとなってしまいます。

  • 役割(責任)=変更理由
  • クラスと複数の役割が結合している場合、分離する。
  • 悪例:ビジネスルールと永続性システム(DB等)の結合
  • (注:コラム筆者は、クラスの構成要素である、メソッドについても同様のルールが言えるのではないか?とも感じております。)
1.b オープン・クローズドの原則(OCP)
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.
ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対して開いて(オープン)いるが、修正に対しては閉じて(クローズド)いなければならない。

「閉鎖開放原則」と呼ばれることもあります。「拡張」に対して開いていて、「変更」に対しては閉じているという状態は、どのような状態でしょうか? 例えばユーザーから機能拡張リクエストがあった場合、当該機能を担当しているクラス、ソースコードを直接して再ビルドしなければならないようなケースでは、変更(機能拡張)に対して閉じていない、OCPに反していることになります。 高度にモジュール化されたシステムでは、この原則が守られてシステム設計されています。例えば、プラグインという言葉を耳にされた方も多いと思います。 機能拡張要求に対して、例えばブラウザ等のソースコードの変更(=再インストール)を必要とせず、プラグインだけを追加すれば機能拡張が実現できるのだとすると、拡張性が維持されていて(開いていて)、ブラウザそのものの修正を拒否出来ている(閉じている)と言えます。

  • 一般的に、「抽象」+「多態性」を巧みに利用し実現されます。
  • 「抽象クラスはそれを実際に実装するクラスとの関係よりも、それを利用するクラスとの関係の方がより密接」であるため、利用側の視点に立った設計が行われるべきです。
  • TemplateMethodパターン(後日紹介予定)では、多態性を巧みに利用しOCPを実現しています。
  • 「先を見越した構造」と「自然な構造」について
    • すべてのケースに適用できる「自然なモデル」など存在しない、という達観も必要です。
    • よって、あらゆる変更に対して完璧に閉じることが不可能です。従って、戦略的に閉じるしかありません。(設計者がどの種の変更に対して自分の設計を閉じたいのかを選択する必要性)
    • 適度な抽象化を、経験・常識に基づいて予測して変更が起きるのを待つしかない!
    • 「仕掛け」を仕込む。最初の弾丸は甘んじて食らっても、同種の弾丸は二度食らわない。(注:経験的に、コラム筆者が好む手法・態度です。想定の範囲内となるような「仕込み」を入れておく、ソフトウェア開発者の機知が光るところではないでしょうか。)
    • 具体的な対応策・手法として:テストファースト・短サイクル開発・優先度の決定・早期頻繁なリリース等。
    • 早まった抽象をしないことも抽象を使うのと同等に重要なこと。。(注:禅問答のようですが、コラム筆者の経験的にも、抽象化の適切な度合いは非常に難しい問題だと思います。)
1.c リスコフの置換原則(LSP)
FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.
基本型に対するポインタ・参照を使用する関数は、派生型に関する知識がない状態でも当該派生型オブジェクトを使用できなければならない。(派生型はその基本型と置換可能でなければならない。)

オブジェクト指向を学習された経験がある方であれば、「IS-A関係」というものを耳にされた機会があると思います。「派生型 IS-A 基本型」、例えば「猫 IS-A 哺乳類」というような関係です。 これはオブジェクト指向における「継承」を考える際に非常に重要な指針となりますが、必ずしもこの例が万能であるとは限らないことをLSPは示唆しており、非常に興味深い原則の1つです。 普段何気なくオブジェクト指向言語に触れられている開発者の方がもしいらっしゃれば、目からウロコの原則かもしれません。原書ないし訳書のご一読をお勧めします。

  • LSPに違反すれば必然的にOCPにも違反してしまいます。
  • IS-A関係が必ずしもLSPを満たすとは限りません。
  • 「正当性」と「本来的な性質」は別物。モデルの正当性は立場(設計者、利用者)によって異なる。
  • 「正方形」 IS-A 「長方形」でしょうか?立場によって答えが異なる、というのがLSPが示唆する本質です。これは「振る舞い」が異なるからであり、LSPに準ずるためには、IS-A関係+合理的に仮定した振る舞いの同等性を考慮しなくてはならない、としています。
  • 「契約による設計(DbC:Design by Contract)」のテクニック:事前条件、事後条件、派生型の事前条件は基本型のそれよりも弱く、事後条件は基本型のそれよりも強くなければならない。(つまり、基本クラスのユーザーがその派生クラスの処理結果に混乱するようなことがあってはならない、ということ。)
  • OCPが有効な時、アプリケーションはより扱いやすく、再利用可能で頑強なものとなる。LSP、DbCはOCPを有効にするための主要な役割を果たすものの1つ。
  • 派生型とは何か?→基本型と完全に「置換出来る」ということ。IS-A関係は実際のところ範囲が広すぎる。「置換出来る」内容については、明示的・非明示的な契約によって定義され、基本型の契約内容は徹底的に理解されなくてはならない、とまとめられています。
1.d 依存関係逆転の原則(DIP)
  • HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
  • ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.
  • 上層モジュールは下層モジュールに依存してはならない。両者は抽象に依存すべきである
  • 抽象が詳細に依存してはならない。詳細が抽象に依存すべきである。

これも非常に興味深い、堅牢なクラス設計を実現するための重要な原則です。 一般的に何かプログラムを作成する場合、まずは機能を実現するための部品(モジュール)を製作し、これをより上位レベルのプログラム(例えばユーザーインタフェースを含むプログラム)から利用する、という構成をとることが多いでしょう。 DIPでは、このような一般的な構造に対して警鐘を鳴らしています。「所有権の逆転」という発想が、非常に重要です。 例えば、C#等ではイベントハンドラという仕組みがありますが、クラスの依存関係とイベントの通知関係が逆転していることがお分かり頂けると思います。(イベントが発生する部品は全体的なユーザーインタフェースによって所有されていますが、イベントの通知の方向は、各部品から全体に対して行われます。) これも、普段何気なくオブジェクト指向言語に触れられている開発者の方がもしいらっしゃれば、目からウロコの原則かもしれません。原書ないし訳書のご一読をお勧めします。

  • 従来の構造化プログラミングでは、上位モジュールが下位モジュールに依存したり、(全体的な)抽象が詳細に依存したりしていました。また、そういった階層構造を作るのが従来手法の目的であったとも言えます。(トップダウン型)
  • DIP→この関係を逆転する。「所有権の逆転」:必要な時にこちらから連絡する。柔軟性・耐久性・移植可能性を実現。
  • 具体的なクラスに依存することの弊害を理解する必要があります。クラスが頻繁に変更されなければ問題ありませんが、自ら作成するクラスはたいてい頻繁に変更されます。
1.e インタフェース分離の原則(ISP)
CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.
クライアントに、利用しないインタフェースへの依存を強制してはならない。

C#のみならず、近年のオブジェクト指向言語では「インタフェース」という機構によって、当該クラスが備えているべき機能のみを定義一覧することが出来ます。 これらが適切に整理細分化されていることで、クラス・インタフェースの利用者にとって使いやすいものになるわけですが、逆に「太った」インタフェース、つまり、余計な定義を含むインタフェースは非常に見通しが悪く使いづらくて、再利用を阻害することになります。

  • 「太った」インタフェースをシェイプアップすることが重要です。

今回は、オブジェクト指向設計原則の初回として、5つの「クラス設計の原則」を取り上げました。
次回以降、「パッケージ凝集度に関する原則」、「パッケージ結合度に関する原則」について取り上げたいと思います。

 

 

プライマル株式会社|コンサルティング事業部


Warning: include(/home/1806248425/primal-inc-com/public_html/common/inc/main_inquiry.html) [function.include]: failed to open stream: No such file or directory in /home/1806248425/primal-inc-com/public_html/column/entry/article000084.php on line 274

Warning: include(/home/1806248425/primal-inc-com/public_html/common/inc/main_inquiry.html) [function.include]: failed to open stream: No such file or directory in /home/1806248425/primal-inc-com/public_html/column/entry/article000084.php on line 274

Warning: include() [function.include]: Failed opening '/home/1806248425/primal-inc-com/public_html/common/inc/main_inquiry.html' for inclusion (include_path='.:/usr/share/pear:/usr/share/php') in /home/1806248425/primal-inc-com/public_html/column/entry/article000084.php on line 274