読者です 読者をやめる 読者になる 読者になる

オブジェクト指向の原則集

近況:米国に1週間予定の出張にいったら5週間に伸びてしまいました。

久しぶりに、ブログを更新しようと思います。

今回は、オブジェクト指向の原則集です。

取り上げる原則は以下の通りです。

  • GRASPパターン(9つ、デザインパターンや他の原則より先に取り上げたい原則(s)です。)
  • クラスの原則(5つ、頭文字をとってSOLIDの原則とも言います。)
  • パッケージの原則(6つ。3つがパッケージ内、3つがパッケージ間のものです。)
  • その他の原則、パターン、用語など(DRY、KISS、YAGNI、BCE図、code smellなど)

簡単な説明、個人的な解釈(丸括弧で記述します)と、関連した記事のリンク等も貼っておきます。

GRASPパターン

  • GRASPは、General Responsibility Assignment Software Patterns の略です。Pは、Principleとする場合もあります。訳すると「汎用的な責務分配のソフトウェアのパターン」でしょうか。書籍やネットの記事によって日本語訳はあいまいなようです。
  • 9つの原則からなっており、クラスやオブジェクトを設計する上でのクラスの分類やクラス間の責任の割り当てについての原則集です。後で説明するSOLIDの原則の一番初めの「クラスの役割は1つでなくてはならない」の役割の付け方に対する指針を与えてくれます。
  • オブジェクト指向を学ぶ上で、デザインパターンや他の原則を学ぶ前に学習したほうがよいと個人的には考えています。)

それでは9つを上げていきたいと思います。
9つすべてを一度に覚えるのは大変なのですが、以下のようにカテゴリに分ける+カテゴリごとの数を記憶しておくと分かりやすいかと思います。

  • "1つのクラス"の「責任の割り当て方に関するもの」カテゴリ(A)が4つ、
  • 親子間や他のクラス間との"複数のクラス間"の継承や多様性などの"クラスの親子間"の「抽象化に関するもの」カテゴリ(B)が3つ、
  • 「結合に関するもの」カテゴリ(C)が2つです。

(覚え方は1つが4、親子、複数が3、結合が2です。英語のWikiだとアルファベット順でしたが、それでは覚えるのはかなり困難です。)
デザインパターンの前に学ぶとよいと思っていますが、この下では、デザインパターンや、BCE分析、MVCアーキテクチャ、3階層アーキテクチャエンタープライズアーキテクチャーパターンまで先取りして説明います。つまり基本的なことで、高度な応用にまで共通の原則なのです。

カテゴリ(A)の4つから。

「情報エキスパート」 'Information Expert'
  • Getter、Setterだけを持つようなクラスや、DBに保存するだけのエンティティを表すクラスです。
  • オブジェクトの静的な分析、クラスの洗い出しをすると登場するクラスです。
  • (一番分かりやすいクラスだと思います。)
「生成者」 'Creator'
  • 情報エキスパートなどを生成するだけの役割を持ったクラスです。
  • デザインパターンのFactoryクラス(この次の純粋人工物でもあります)でよく実装されます。
  • (これも分かりやすいクラスかと思います。)
「純粋架空物、純粋人工物、ピュアファブリケーション」 'Pure Fabrication'
  • クラスの洗い出しでは登場しませんが、プログラムを作る上であると便利な架空の人工的なクラスです。
  • アプリケーションクラスや、3階層モデルのビジネスロジック層の「サービス」クラスなどが例です。
  • (これは個人的には後から学んだものです。)
「コントローラー」 'Controller'
  • 他のクラス間のイベント処理や操作、処理を担当するクラスです。英語のWikiには「ユーザ」のCRUD操作を行う「UserControl」クラスが例として書かれています。
  • 他にはMVCアーキテクチャ(Model,View,Controller)のModelの変更をViewに伝えるController、少し古いパターンですがエンティティを保存するDAO(Data Access Object)パターン(最近はFacadeでしょうか)が例です。
  • (複雑になりやすいビジネスロジックの分離、間接化を行う純粋架空物でもあります。)

カテゴリ(B)

多様性ポリモーフィズム」 'Polymorphism'
  • クラスごとに処理が異なるような場合には、「継承」を導入して対応します。オブジェクト指向の用語として学ぶ「ポリモーフィズム」です。
  • デザインパターンでは、Template Method、Adapterパターンなどで使われています。Javaだと、各クラスごとに比較の方法を提供するinterfaceのComparatorなどが例だと思います。
「間接化」 'Indirection'
  • 2つ以上のクラス間や1つのクラスで属性と振る舞いを持たせる際に複雑な処理が発生するような場合に導入する、間接化を行うクラスです。
  • 上記DAOパターン、ビジネスロジックを担当するサービスクラスが例です。
  • (たとえば図書館アプリケーションなどでUserクラスやBookクラスに複雑なDB保存処理を書くと、DBへの依存をしてしまうので専門家であるDAOを導入します。)
「バリエーション保護」 'Protected Variations'
  • 安定した個所と不安定な個所を分離するために導入する方法です。
  • 変更が入りやすい部分を抜き出したクラスです。
  • たとえばエンティティやDAOは変更が入りにくいですが、ビジネスロジックのサービスは変更が入りやすいです。

カテゴリ(C)

疎結合」 'Low Coupling'
  • 複数のクラス間の結合度は低くしようという指針です。
  • 結合度が高いと後述するSOLIDの原則のOに当たるオープンクローズの原則に違反することになります。
  • たとえば上記でも挙げたDAOクラス(DBへの依存度が高いクラス)をただのエンティティであるUserクラスやBookクラスに実装すると、DBが変更された際に、エンティティクラスすべてに変更が発生します。
「高凝集」 'High Cohesion'
  • 1つのクラス内での役割を凝縮しましょうという指針です。
  • 凝集性が低いとSOLIDの原則のSに当たる単一責務の原則に違反することになります。
  • 例を挙げるのは難しいですがやはり上記で挙げているMVCのControllerが無いと、GUIへの変更処理をModelが持つか、Viewが持つかの責務、役割があいまいになります。

クラスの原則、SOLIDの原則

  • 1つのクラスや複数間、親子間のクラスを作る上での指針となる5つの原則集です。
  • それぞれの英語の頭文字を取って、「SOLIDの原則」と呼ばれます。3文字で表す場合もあります。
  • (Lに当たる「リスコフ置換原則」は人名のうえ、そのままで日本語になっているので、概念として覚えろと言われてもひどく覚えにくいものだと思います。)
単一責務の原則 'Single Responsibility Principle (SRP)'
  • 原著、参考URLには「クラスを変更する理由は1つであるべきだ。(A class should have only one reason to change.)」とあります。
  • (個人的にはこれは分かりにくいと思っています。私はいつも、上記GRASPパターンでいうところの「責務」を1つだけ与えましょう、クラスの説明をする際に役割や責任は1行で表せるものが望ましい、1つの変更が発生する際には1つのクラスにだけ変更が入るのが望ましい、という原則としてメンバには説明しています。)
オープンクローズドの原則 'Open Closed Principle (OCP)'
  • クラスやパッケージは変更や追加に対してはオープンで、修正に対しては閉じていなくてはならないという原則です。
  • Model、Viewに処理を書いたり、UserやBookにビジネスロジックを書いてしまうと仕様変更時に複数のクラスに変更が発生するので、Controllerやサービスクラスを導入し拡張やバグ修正は閉じたものにしましょうというものと解釈しています。
リスコフ置換原則 'Liskov Substitution Principle (LSP)'
  • 派生クラスと基本クラスは、交換可能で無くてはならないという原則です。
  • 「派生クラスで、基本クラスの前提条件を壊してはならない」というものですが、なかなか簡単にわかりやすい例はネットサーフィンしましたが見つかりませんでした。私の身近で具体的に発生した事例を挙げておきたいと思います。
  1. 基本クラスAで、アルファベットのみを受け入れ、それ以外で例外を発生するcheckInput(String input)メソッドがあったとします。
  2. 派生クラスBで、数字も受け入れるようにしてしまいます。
  3. ClassA a = new ClassB()という書き方ができ、a.checkInput("abc123")を呼び出すと、例外を発生する責務を果たしていません。

キャストできるので、責務は破らないようにしましょうという原則です。

インターフェース分離の原則 'Interface Segregation Principle (ISP)'
  • 太ったインターフェースを使うのではなく、粒度の細かなインターフェースを使うべきという原則です。
  • クライアントとなるクラスやインターフェースが複数あって、それぞれ必要なメソッドが違うのに、1つのインターフェースで実装してしまうと、関係のないメソッドの影響を受けかねないというものです。
依存性逆転の原則 'Dependency Inversion Principle (DIP)'
  • 上位モジュールが下位モジュールに依存してはならない、抽象が実装に依存するのではなく実装が抽象に依存すべきという原則です。

ダメな例:

  • 上位モジュールが、必要なメソッドを下位モジュールが提供しています。
  • 下位モジュールで、たとえばシグニチャの修正が発生したとき、上位モジュールを修正しなくてはなりません。

依存性を逆転させた例:

  • 上位モジュールが必要なメソッドのみを含む、抽象であるインターフェースを用意します。
  • 下位モジュールはそのインターフェースを実装します。
  • こうすると、上位モジュールがシグニチャの所有権を持つことになり、依存性が逆転します。

Javaでは、DIコンテナで下位モジュールの実装クラスを生成したりします。CDIの@Injectなどもそうです。

SOLIDの原則の参考URL

パッケージの原則

  • JarのパッケージやDLLなどのライブラリを分割する際の6つの原則です。パッケージ内の原則3つ、パッケージ間の原則3つから成ります。
  • 最初の3つがパッケージ内、後ろの3つがパッケージ間の原則です。
再利用―リリース等価の原則 'Reuse-release equivalence principle (REP)'
  • パッケージを利用する場合や、リリースする場合は1つにまとめよという原則です。
  • 破ったときどうなるか:複数のパッケージを同時にリリースする管理の手間が増え、修正時にバージョン違いが発生する危険を負います。
  • 破ったとき解決するか:1つのパッケージにまとめましょう。
全再利用の原則 'Common-reuse principle (CRP)'
  • パッケージを再利用する単位でまとめよという原則です。
  • 破ったときどうなるか:これも、複数のパッケージを同時にリリースすることになります。
  • 破ったとき解決するか:これも、1つのパッケージにまとめましょう。
閉鎖性共通の原則 'Common-closure principle (CCP)'
  • パッケージに修正が入る場合は、1つのパッケージだけに閉じるようにせよという原則です。
  • 破ったときどうなるか:複数のパッケージにまたがる修正や、バージョン違いを使う危険を負います。
  • 破ったとき解決するか:やはり、1つにまとめます。
非循環依存関係の原則 'Acyclic dependencies principle (ADP)'
  • パッケージ間の依存は1方向になるようにせよ、循環依存させてはならないという原則です。
  • 破ったときどうなるか:依存関係がごちゃごちゃなので、1つのパッケージに修正が入った場合に同時に修正をする必要があります。REP、CRP、CCPに反します。
  • 破ったとき解決するか:インターフェースを用意しそれにだけ依存させる(DIPを使う)、両方に依存する間接パッケージを用意する。(Indirectionを使う)
依存性安定の原則 'Stable-dependencies principle (SDP)'
  • 依存関係が多いものは不安定、依存関係が少ないものは安定しているので、安定しているものに依存せよという原則です。
  • 破ったときどうなるか:不安定な仕様変更にひっぱられて、安定しているものが修正する羽目になります。
  • 破ったとき解決するか:不安定な側もしくは安定した側はインターフェースを提供し、実装は分離します。(DIPを使う)
抽象性安定の原則 'Stable-abstractions principle (SAP)'
  • 安定しているものは、抽象度も高くせよ(不安定なものは具現度を高くせよ)という原則です。
  • 破ったときどうなるか:不安定な側への影響が発生します。
  • 破ったとき解決するか:やはり、DIPを使います。

私の身近にあった具体的な事例をあげると、

  1. OracleのDBにしか対応していないDAOがありましたが、他のDBにもする必要が発生しました。
  2. 共通のインターフェースのみを抽出し、他のモジュールはそのインターフェースのみ(つまり、抽象度の高い安定したものを用意した)を使用するように修正し、
  3. そのパッケージにファクトリを用意し、そのファクトリから具現化クラスを生成させるようにしました。
  4. こうすることでRDBMSに依存した処理(不安定で、具現化された実装)を分離し、他への影響を与えないようにできます。
参考URL

その他の原則

DRYの原則
  • Don't Repeat Yourself の略
  • 同じコードや同じ開発は2度しないこと。
  • 似たものに、Once and only onceという原則もあります。
KISSの原則
  • Keep It Simple Stupid の略
  • シンプルさを保つこと。複雑さはバグやコントロール不能なものを生み出します。
YAGNIの原則
  • You Aren't Gonna Need It の略。
  • 要らない、使わない機能を作る必要はありません。本当に必要になってから作ること。
  • 下記URLにもありますが、「作るコスト」、「直す、削るコスト」、「走らせる(メンテナンスする)コスト」、「その分、遅れるコスト」と無駄しか生みません。
  • 参考URL1:Martin Fowler氏が"Yagni: You Are Not Gonna Need IT"について語る 日本語の解説です。
  • 参考URL2:Yagni 原文です。図だけでも無駄は分かりやすいです。
デメテルの法則 Don't talk to stranger
  • a.b.method()はダメで、a.method()までにしましょうという原則です。
  • 会話するクラスが増えれば増えるほど、依存関係が増えます。
  • 個人的には、極端すぎるのではないかと思いますが、依存関係を減らすこと、クラスを増やしすぎたり、大きくしないことのバランスが重要だと思います。
TMTOWTDI
  • There's More Than One Way To Do It 「それをやり方は1つ以上ある」
  • PerlRubyのモットーです。Rubyは最重要とはしていないそうです。
  • 逆にPythonには、'There should be one-- and preferably only one --obvious way to do it.' 「1つだけ明白なやり方であるべき」
  • 個人的には大枠の設計や実現方法に関しては複数のパターンやアーキテクチャがあってもよく、コードの書き方に関しては1つだけになるような言語が好みです。設計は人に依存するし優秀な人の設計は見たいところです、また、コードのメンテナンスは人に依存したくありません。
車輪の再発明
  • すでにあるものを開発する必要はないというものです。
  • ひどい例ですが、Javaの.propertiesファイルをBufferWriterを使って解析するコードを書いているのを見たことがあります。正直笑えませんでした。
Code Smell(コードの匂い)
BCE図、ロバストネス分析

ちなみに、この記事を書くきっかけは数か月前ですがパッケージの循環依存をやってしまったメンバとリスコフ置換原則を破ったメンバがいたからです。単語くらいは覚えておくと便利ですとレビュー時に言ったのですが、最近の若手は「ふーん」という感じで軽く流されました。もうあまり流行っていないようです。