オブジェクト指向設計実践ガイドの各章を備忘録としてまとめていく。
オブジェクト指向設計
新しくアプリケーションを作る際にまずは設計を行う。では、なぜ設計を行うのか?これは、そのアプリケーションが現在の要件を満たすだけで終わりではなく、将来起こりうるであろう新しい要件を満たすためにアプリケーションへの変更をしやすくするためである。
変更に耐えうるアプリケーションを作るためにはオブジェクト同士の依存関係を適切に管理しなければならない。
単一責任クラスの設計
実装する手段は分かっているけど、その実装をどの部分に置いてどのように構造化(クラス分け)させればいいのか?そして、最終的にできる限り可変性のあるコードを書くにはどうすれば良いか?
変更のしやすさの定義
- 見通しが良い: 変更がもたらす影響が明白
- 合理的: 変更へのコストに対し変更がもたらす利益が多い
- 再利用しやすい: 予期しない環境でも再利用できる
- 模範的: コードを変更する人が自然に上記の品質を保てるコードになっている
2つ以上の責任を持つクラスはどうしてダメなのか?
複数の責任を持つクラスを再利用する場合について考える。
複数のうちの1つだけの責任を持つクラスが必要なときに、いらない責任まで持つクラスを使うことになってしまう。
また、クラス内に変更を加える場合はどうだろうか?
複数の責任がクラス内部で絡み合っているせいである部分への変更が色々な部分に影響してしまう可能性が高まる。
では、どのようにして複数の責任を持つクラスを見つけることができるのか?
- クラスの持つメソッドを質問に言い換える
- クラスを1文で説明する
1に関しては、
[クラス名]さん、[メソッド名]を教えてくれませんか?
という文章に違和感がないか
2に関しては、
~と~をする、~または~のクラス
のような文章になっていないか
では、いつ設計を変更するべきなのか?
現時点で変更のしやすさを保っているならそのままで良い。
将来新たな要件が発生し、依存関係が発覚した時点で変更を加えるのが良い。
現時点でのプロジェクトに対する知識と、将来における知識とでは必ず将来における知識の方が多い。それゆえ、設計の変更を将来に延期させることで、優れた設計を決定するための必要な情報に基づいて設計の変更が行える。
今現在できる変更を歓迎するためのコードテクニック
- インスタンス変数の隠蔽
- データ構造の隠蔽
メソッドもクラスと同様単一の責任であるべき。
理由は、クラスの時と同様に再利用性が高まるからであり、単一かどうかの見極めもクラスの時と同じ手法が使える。
また、メソッドを単一責任にすることで、そのメソッドを持つクラスを明確にし、本当に単一責任であるかの見極めにもなる。
以下に、単一責任のメソッドがもたらす恩恵を挙げる
- 隠蔽されていた性質を明らかにする
- コメントが必要なくなる
- 再利用を促進する
- 他のクラスへの移動を簡単にする
依存関係の管理
前章は、クラス自身が独自に実装する振る舞いを管理する方法を扱った。ここでは、振る舞いが他のオブジェクトに実装されている時の設計について考える。
振る舞いが他のオブジェクトに実装されている時、そのオブジェクトについて知っていなければならない。この知っているということが依存を作り出す。複数のオブジェクトに強く依存するオブジェクトは実質複数のまとまった1つのオブジェクトのように振る舞っているのと変わらない。
一般に、オブジェクトが次のものを知っている時、依存関係が生まれている。
- 他のクラスの名前
- self以外のどこかに送ろうとするメッセージの名前
- メッセージが要求する引数
- 引数の順番
オブジェクト指向設計においてこれらの依存関係を含んでしまうのは避けられないが、以下にこれらの依存関係を少なくしていくかが大事。依存関係を不必要に生んでしまうことで依存しているオブジェクトの変更の影響を受けることになり変更へのコストが高まる。
これらの依存関係の回避方法について述べていく。
依存性の注入
特定のクラス名をハードコーディングしている時、それは暗に「そのクラスとしか作業しない。それ以外のクラスとの作業は拒絶する。」と宣言しているものである。しかし、元のオブジェクトが必要としているのは、そのクラスの名前ではない場合が多い。本当に必要なのは、メッセージである。そのメッセージに応答でき、求めてる振る舞いをするクラスであるならばなんでも良い。
このことは元のクラス内でクラス名をハードコーディングするのではなく、求めてるメッセージを持つオブジェクトを受け取ることで解決できる。つまり依存オブジェクトの注入である。
外部メッセージの隔離
外部メッセージとは、self以外に送られるメッセージのこと。
この外部へ送るメッセージの処理をselfに別のメソッドとして実装することで、DRYを可能にし変更に強くなる。
引数の順番への依存の回避
引数が必要なメッセージを送る時、送り手は引数の内容についての知識と引数の順番についての知識が必要になる。ここでは特に後者についての回避策を述べる。
引数にハッシュ、キーワード引数を使う
ハッシュのキーへの依存が残るが、これは引数の順番に比べて変更が強制されるリスクは少ない。また、このキー名が引数に関する明示的なドキュメントになり得るという副産物もある。
これは、常時適応すべきことではなく、メソッドにとて非常に安定性の高い引数を持っているなら固定順に引数を受け取り、安定性のない引数についてはハッシュで受け取るという戦略が好ましい。
これまでは、不必要な依存関係を取り除く方法を見てきた。次に、この依存関係の方向について調べていく。オブジェクトはどのようなものに依存するべきだろうか?将来の変更に耐え得る設計を目指すのであれば自分より変更される可能性の低いものに依存するのが良さそうだ。次に、コードに関する一般的な事実をリスト化する。
- あるクラスは他のクラスより要件が変わりやすい
- 具象クラスは、抽象クラスよりも変わる可能性が高い
- 多くのところから依存されているクラスを変更すると広範囲に影響が及ぶ
これらは、「依存しているものの数」 と「変わりやすさ」について述べてる。この2つの組み合わせによっては、とても危険な設計を示唆するものとなる。
- 「依存しているものの数」が多く、「変わりやすさ」が低い -> 抽象領域
- 「依存しているものの数」が少なく、「変わりやすさ」が低い -> 中立領域
- 「依存しているものの数」が少なく、「変わりやすさ」が高い -> 中立領域
- 「依存しているものの数」が多く、「変わりやすさ」が高い -> 危険領域
3番目の中立領域は、抽象領域と真逆の組み合わせで具象領域を表している。
4番目は設計において最悪。これを避けるためには常に自分より変更されないものに依存するという規約を守らなれければならない。設計の際立ち止まってこの領域分けを行うことは、危険領域に属するクラスの誕生を回避するのに役立つ。
柔軟なインターフェース(重要)
今までは、クラスそのものに注目しアプリケーションはクラスの集まりのように考えてきた。しかし、アプリケーションはクラスから成り立つのではなくメッセージによって定義されるものである。クラスは何が入るかを制御し、メッセージは実際の動きをアプリケーションに反映させる。したがってここでは、オブジェクト同士がメッセージをやり取りするためのインターフェースについて考える。
なぜ、オブジェクト同士が複雑に絡み合う関係が生まれるのだろうか?これは、単一責任の失敗や依存性の注入の失敗だけに起因するものではない。クラスで所有するすべての物(メソッド)を明らかにすることに起因する場合もある。では、クラスは何を明らか(パブリックインターフェース)にし、何を隠蔽(プライベートインターフェース)するべきだろうか。
それぞれの特性について述べる。
パブリックインターフェース
- クラスの主要な責任を明らかにする
- 外部から実行されることを想定している
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで文書化されている
プライベートインターフェース
- 実装の詳細に関わる
- 他のオブジェクトから送られてくることを想定しない
- いかなる理由でも変更され得る
- 他者が依存しては危険
- テストで言及されない場合がある
この2つのインターフェースを使い分けるには設計において、クラスを中心とする設計から、メッセージを中心とする設計が役に立つ。これは、「このクラスの必要性はわかるが、これは何をすべきなんだろう?」から「このメッセージを送る必要があるが、誰が応答すべきなんだろう?」という問いへ変わる。
このメッセージに基づく設計をする際、次のことに気をつけると良い。
「どのように」を伝えるのではなく、「何を」を頼むようにする
「どのように」を伝えることは、相手に関する知識を増やすことに繋がる。例えるなら、子供が母親にご飯を作ってもらう際に、「目玉焼きを作るために卵を割って、かき混ぜて〜」とご飯を作る過程を指示することである。そうではなく、「何を」を伝えることによって相手に関する知識を減らすのである。これは、子供が母親に「ご飯作って」と頼むような感じである。これにより子供は母親がどのようにご飯を作るかの知識を持たなくても良い。子供がわがままであればあるほど、設計の観点からは良いのである。
また、より良い設計を目指すためには、そのオブジェクトがコンテキストから完全に独立するのである。コンテキストを持つことによって、そのオブジェクトを使用する際、コンテキストに関わるオブジェクトに依存しなければならない。このコンテキストを排除することで予期せぬ相手との共同作業も可能にする。そのために、することが前章で扱った「依存性の注入」である。
以上なより良い設計、メッセージの内容と送る相手を模索し改善していのに役立つツールとしてUMLのシーケンス図がある。
ダックタイピング
ダックタイピングとは、共通のパブリックインターフェースを持つものであればクラスに寄らずアクセスできる構造のこと。
単一の目的を果たす様々なクラスのパブリックインターフェースを共通化することで、このインターフェースを必要としている側はその目的を果たすクラスを判別することなくメッセージを送ることができる。この抽象化は具象化より少なからず理解力を伴うが、変更の際のコストを大きく下げることができる。
このことは、今まで見てきた通りのクラスを中心とした設計ではなく、何をするかという振る舞いを第一とした設計に基づくものである。
では、このダックタイプを見つける基準はなんだろうか?以下に隠れたダックの存在を示唆するものを並べる。
- クラスで分岐するcase
- kind_of? is_a?
- responds_to?
最初の2つについては、いずれもそれぞれのクラスに共通する処理のインターフェースが共通化されていないために分岐が必要になってしまう。最後は、クラス名に依存するべきでないことを理解しつつもやはり処理のインターフェースが共通化されていないことに起因する。
rubyではこのようなダックタイプはコード上では仮想的に成り立つものである。故にパブリックインターフェースの文書化とテストが必要となる。これは、優れたテストは文書化でもあるのでテストを書くことで達成できる。
継承
継承は、自らが理解できなかったメッセージに対して他のクラスに自動委譲する仕組みのこと。
では、どういったときに継承を使うことが適切で間違った使い方はどのようなものであるか?
継承はその仕組みの通り、スーパークラスにメッセージを自動委譲するので、あるクラス間に共通する振る舞いを共有したい場合に有効である。共通の振る舞いをスーパークラスに実装し、サブクラスにはそのクラスに特化した実装をするのである。
継承が効果を発揮するためのルールを2つ示す。
- 汎化ー特化の関係が成立していること
- 正しいコーディングテクニックを用いていること
1つ目の汎化ー特化の関係を築くための過程を見ていく。
まず、サブクラスを1個しか持たないようなスーパークラスは意味をなさない。3個以上のサブクラスが必要な段階においてスーパークラスの実装を考えるべきである。これは、サブクラスにすべき対象が増えるにつれて正しく抽象を見つけるための情報量が増えるからである。
では、抽象クラスを作成する段階に訪れた時どのような手順をすべきだろうか?
まず、元のクラスを具象クラス(サブクラス)として降格させ、この具象クラスから抽象をスーパークラスへと昇格させるのである。これは、抽象から具象を抜き出すことの難しさに起因する。もし抜き出す必要のある具象を見落としてしまった場合そのスーパークラスはもはや抽象クラスではなくなる。
2つ目のコーディングテクニックについて見ていく。
スーパークラス内で基本の構造(共通のアルゴリズム)を定義し、サブクラスで何か特化した処理をする機会を与えるのである。これは、テンプレートメソッドパターンと言われる。
このテンプレートメソッドを使う際には、どのクラスにおいても必ず実装を用意する必要がある。これは、スーパークラスのテンプレートメソッド内にエラーを発生させる処理を書くことで、サブクラスがそのメソッドを実装しなかったとき原因を追及できるようなメリットがある。
次に、継承によるサブクラスとスーパークラスの依存度(結合度)について見ていく。
今までのようにあるクラスは他のクラスへの依存は少ない方が良い。これは継承による2つのクラス間でも適応される事柄である。サブクラスでよく使われるsuperメソッドは強い依存(結合)を表す。サブクラス内でsuperを忘れた時に発生するエラーのデバッグは辛く、またこれよりもひどいのがsuperを忘れて期待する結果にならないのにエラーが発生しない場合である。
superメソッドを使わないようにするにはどうしたら良いか?これは、サブクラスがアルゴリズムを知ることを一旦許し、スーパークラスが代わりにフックメッセージを用意することでいくらか疎結合にすることができる。これにより、サブクラスではオーバーライドすることなく、アルゴリズムに特化したメソッドを加えることができる。
モジュール
ダックタイプでは、メッセージシグネチャを共有するがそれぞれの振る舞い、実装は独自のものを持っていた。ここでは、メッセージシグネチャだけでなく、特定の振る舞いも共有する方法を考える。
これを可能にするのがモジュールである。クラスからは独立し、どんなオブジェクトにも混入させることができる。継承の時と同様に、あるオブジェクトが受け取った理解できなかったメッセージを自動以上する仕組みを持つ。
モジュールを使用する際の注意点
抽象スーパークラス内のコードを使わないサブクラスがあってはいけないのと同じように、モジュールのコードを一部だけしか使わないようなオブジェクトがあってはならない。
サブクラスはスーパークラスのインタフェースに含まれるどのメッセージにも対応可能であるように、モジュールの場合もモジュールをインクルードするオブジェクト達をそれぞれ入れ替えてもロールを担えるようにしなくてはならない。
継承と同じようにテンプレートメソッドパターンを使うことで抽象のアルゴリズムをモジュールで用意し、特化はインクルードする側のオブジェクトで行うようにする。
コンポジション
クラスを部品として扱い、それを組み合わせ全体として構築していく構造。
これはコンポーズするオブジェクトとコンポーズされるオブジェクトとの関係に「has-a」が成立する。
厳密にいうとコンポジションとは、包含される側のオブジェクトが包含する側のオブジェクトから独立して存在し得ない。例として、「大学ー学部」の関係がある。
また、包含される側のオブジェクトが包含する側のオブジェクトから独立して存在し得る関係を定義するのが「集約」である。これは、「学部ー教授」の関係がある。
では、継承とコンポジションどちらを選択するべきか?コンポジションで解決できるものであればまずはコンポジションを使用するのが良い。継承はコンポジションより多くの依存を含むのだ。
継承とコンポジションの選択については、p229を参照
参考本
オブジェクト指向設計実践ガイド Rubyでわかる進化し続ける柔軟なアプリケーションの育て方