<輪読会> オブジェクト指向実践ガイド -第8章コンポジションでオブジェクトを組み合わせる-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第8章コンポジションでオブジェクトを組み合わせる
コンポジションとは
- 組み合わされた全体が、単なる部品の集合以上となるように、個別の部品を複雑な全体へと組み合わせる(コンポーズする)行為のこと
この章の目的
8.5コンポジションと継承の選択
継承による影響を認める
継承の利点
- 「合理的であること」
- 振る舞いの大きな変更を、コードの小さな変更で成し遂げられる
- 「利用性が高いこと」
- 新たなサブクラスを既存の階層構造へ追加するとき、既存のコードへの変更はまったく必要がない
- 「模範的であること」
- 階層構造(クラスの親子間)における既存のパターンはかんたんに倣うことができる
- 「合理的であること」
継承のコスト
継承はケースバイケースである(以下具体例)
たとえば社内向けのアプリケーションのコードを書くとしましょう。しかも、それは自分にとって本当に慣れ親しんだ問題領域のアプリケーションだとします。この場合、抱える設計課題にとって、継承が費用対効果の高い解決法であると自信を持てるぐらい、未来を十分に予測できるかもしれません。しかし、より幅広い人たちにコードを書くほど、将来の要求を予期する能力は必然的に下がっていきます。そして、インターフェースの一部として継承を必要とすることの適切さも下がっていくでしょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.5119-5124). Kindle 版.
コンポジションの影響を認める
コンポジションによってつくられたオブジェクト
- クラス階層の構造には依存しない
- 自身のメッセージは自身で委譲する
- 上記の違いが、異なるコストと利点をもたらす
コンポジションの利点
- 責任が単純明快で、明確に定義されたインターフェースを介してアクセス可能な小さなオブジェクトが作られる
- 小さなオブジェクトは単一責任を持つ
- コードは簡単に理解でき、変更が起きた場合に何が起こるか明確である
- 上の階層構造にあるクラスへの変更によって生じる副作用の影響を受けにくい
- 自身のパーツと、インターフェースを介して関わることで、抜き差し可能で、拡張性が高く、変更にも寛容なオブジェクトが作られる
- 責任が単純明快で、明確に定義されたインターフェースを介してアクセス可能な小さなオブジェクトが作られる
コンポジションのコスト
- コンポーズされたオブジェクトは多くの小さなオブジェクトに依存してしまい、組み合わせられた全体の動作は、理解しやすいとはいえない
- 明示的にどのメッセージをだれに委譲するかを必ず知っていなければならない
- 同一の委譲のコードが、いくつもの多岐にわたるオブジェクトによって必要とされる可能性がある
- コードを共有するための手段は何も提供してくれない
8.6まとめ
- 継承が良い場合
- 過去のコードの大部分を使いつつ、新たなコードの追加が比較的少量のときに、既存のクラスに機能を追加する場合(is-a関係)
- コンポジションが良い場合
- 振る舞いが、それを構成するパーツの総和を上回る場合(has-a関係)
総和《名・ス他》幾つかの数・量の全部を加えた合計。総計。
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第7章モジュールでロールの振る舞いを共有する-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第7章モジュールでロールの振る舞いを共有する
- 継承で解決できない問題
- 例えば、2つのサブクラスに共通する性質を組み合わせする際、単純な継承では解決できない
- 上記の場合、継承の手法を使い「ロール(役割)」を共有する解決法がある
7.1ロールを理解する
問題によっては、以前には関連のなかったオブジェクト同士に共通の振る舞いを持たせなければならない場合があり、この共通の振る舞いをクラスと直交させる役割を「ロール」という
この依存関係を最小にする方法を本節で学ぶ
ロールを見つける
モジュールとは
- さまざまなクラスのオブジェクトが、1カ所に定義されたコードを使って共通のロールを担うための方法
- メソッドはモジュール内に定義することができる
- 任意のオブジェクトに追加することができる
- さまざまなクラスのオブジェクトが、1カ所に定義されたコードを使って共通のロールを担うための方法
オブジェクトが応答できるメッセージの集合には、以下のようなものがある
- 自身が実装するメッセージ
- 自身より上の階層の、すべてのオブジェクトで実装されるメッセージ
- 自身に追加される、すべてのモジュールで実装されるメッセージ
- 自身より上の階層のオブジェクトに追加される、すべてのモジュールで実装されるメッセージ
応答できるメッセージの集合が恐ろしいほど大きく、混乱してしまうのではと心配になったのであれば、問題を明確に把握していると言えます。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.4050-4051). Kindle 版.
上記は現状の問題提起であり、この後に解決していくようなことである。
抽象を抽出する
- モジュールと継承の使用方法の違いとは何か(個人的な解釈)
- 影響範囲は別であり、基本的には継承ベースで考えると良い
- 継承関係が複雑になったり、影響力が大きくなるときは、モジュールを使用する
- 継承とモジュールは実装の具体的な方法は違えど、考え方はほぼ同じである
- 継承の場合は、複数のサブクラス
include
などの記述をしなくても良いし、class
の宣言部分を見れば関係が明確である
メソッド探索の仕組み
- ざっくりとした流れ
適切なメソッドを見つける試みがすべて失敗に終わった場合は、そこで探索が終わると思うかもしれません。しかし多くの言語には、メッセージを解決するための第二の方法が備わっています。Rubyは、最初にメッセージを受けたオブジェクトに新しいメッセージ(method_missing)を送ることで、2回目のチャンスを与えます。method_missingには同時に引数が渡されます。ここでは:sparesです。今度は、この新しいメッセージを解決するための探索が行われます。まったく同じ道筋をたどって探索が行われますが、今度の探索の対象はsparesではなくmethod_missingです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.4319-4326). Kindle 版.
- より正確な流れ
- ほぼ完全な流れ
ロールの振る舞いを継承する
使用方法には気をつける
これらを下手に使えば、本当に酷いコードを書けてしまいます。やるかどうかは別として、モジュールにモジュールをインクルードするコードを書いたり、モジュールに定義されたメソッドを上書きするモジュールをつくったりすることもできます。クラスによる継承を深くネストした階層構造をつくり、さらに、いま挙げたようなさまざまなモジュールを階層構造のあちこちにインクルードすることだってできるのです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.4383-4387). Kindle 版.
7.2継承可能なコードを書く
継承される振る舞いを共有するという設計の時の特有の難しさをみていく
アンチパターン
- 継承を使って書いた方が良いとされるアンチパターンとその対策
オブジェクトがtypeやcategoryという変数名を使い、どんなメッセージをselfに送るかを決めているパターン
- (対策)共通のコードは抽象スーパークラスにおき、サブクラスを使って異なる型をつくる
- (対策)上記の構成にすることで、サブクラスを追加すれば新たなサブタイプ(派生型)を作れるようになる
- (対策)これらのサブクラスが、既存のコードを変えずに階層構造を拡張してくれるようになる
メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン
- (上記の問題点)受け取る側のオブジェクトを増やすたびに、毎回コードを変更せねばならない。この場合、受け手になり得るオブジェクトは、どれも共通のロールを担っており、このロールはダックタイプとしてコードに落とし込まれるべきであり、受け手のオブジェクトはダックタイプのインターフェースを実装するべき
- (対策)受け手のオブジェクトが複数あったとしても、送り手のオブジェクトとしてはどのオブジェクトに対しても1つのメッセージを送る
- (対策)どれも同じロールを担っているので、共通のメッセージを理解するだろうと確信してメッセージを送れるようになる
- (上記の問題点)受け取る側のオブジェクトを増やすたびに、毎回コードを変更せねばならない。この場合、受け手になり得るオブジェクトは、どれも共通のロールを担っており、このロールはダックタイプとしてコードに落とし込まれるべきであり、受け手のオブジェクトはダックタイプのインターフェースを実装するべき
抽象に固執する
抽象スーパークラス内のコードを使わないサブクラスがあってはなりません。すべてのサブクラスでは使わないけれど一部のサブクラスでは使うというようなコードは、スーパークラスに置くべきではないのです。この制約は、モジュールでも同様です。モジュールのコードを一部だけ使うようなオブジェクトがあってはなりません。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.4416-4419). Kindle 版.
契約を守る
オブジェクトが期待どおりに振る舞うときで、かつ、サブクラスがスーパークラスのインターフェースに一致するように「期待される」ときのみである
テンプレートメソッドパターンを使う
テンプレートメソッドパターンを使うと、抽象を具象から分けることができる。 抽象コードでアルゴリズムを定義し、抽象を継承する具象では、テンプレート化されたメソッドをオーバーライドすることで特化を行う
前もって疎結合にする
- 継承する側でsuperを呼び出すようなコードを書くのは避けるべき
- 代わりにフックメッセージを使う方が良い
フックメソッドは、superを送る問題は解決しますが、残念ながら、階層構造の階層が隣接していなければ役に立たない
階層構造は浅くする
- 階層構造を浅く保つべき理由
- 探索パス上にオブジェクトが多いとメッセージが通過するオブジェクトも増えます。すると、振る舞いが追加される機会も格段に増えます。オブジェクトは自身より上に位置するオブジェクト「すべて」に依存するので、階層構造が深ければ、その分だけ大量の依存をはじめから持つことになる
7.3まとめ
- (7.2 ※アンチパターン)の考えを用いて、継承かモジュールを使用することを検討する
- 継承とモジュールの使い分けに関しては、リスコフの置換原則を用い、メソッド探索の仕組みを考慮して、どちらで実装するかを検討する
- 継承、モジュールを実装する具体的な方法としてテンプレートメソッドパターンを使用すると好ましい
- superを使用しないことやフックメッセージを使用することで疎結合にする
- 階層構造を深くしないことで、不要な依存関係を取り除くことができる
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第6章継承によって振る舞いを獲得する-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第6章継承によって振る舞いを獲得する
- オブジェクト指向設計の振り返り
適切に設計されたアプリケーションは、再利用可能なコードから構成されます。最小限のコンテキストと明確なインターフェース、そして依存オブジェクトが注入されている、小さい自己完結型のオブジェクトは、本質的に再利用可能です。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3089-3091). Kindle 版.
- 本章の目的
この章では、継承を適切に使ったコードをどのように書くかを、詳細な例を用いて示します。目標は、確かな専門的根拠に基づいた継承階層のつくり方を説明することであり、目的は、読者がしかるべきときに継承を選択できるようにすることです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3095-3097). Kindle 版.
6.1クラスによる継承を理解する
継承とは、根本的に「メッセージの自動委譲」の仕組みのことである
継承をさらに詳しく
理解されなかったメッセージに対して、転送経路を定義するものです。継承は関係をつくります。あるオブジェクトが受け取ったメッセージに応答できなければ、ほかのオブジェクトにそのメッセージを委譲する、というような関係です。明示的にメッセージを委譲するコードを書く必要はなく、2つのオブジェクト間の継承関係を定義するだけで、転送は自動的に行われるようになります。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3102-3107). Kindle 版.
スーパークラスとサブクラスとは Rubyでは、定義済みのクラスを継承して新しいクラスを作成できます。 継承元のクラスをスーパークラス(親クラス)、継承して作成したクラスをサブクラスといいます。 似たような処理を行うクラスや使用頻度の高いクラスなど、共通部分のあるクラスを利用するために継承します。
6.2継承を使うべき箇所を識別する
本節では、継承を使った方が良いと気付く為の方法についての説明がされる
複数の型を埋め込む
このif文は、「自身の分類を保持する属性」を確認し、それに基づき「自身」に送るメッセージを決定するものです。これによって思い出されるのは、前の章でダックタイピングを扱ったときに議論したパターンです。そこで見たif文は、「オブジェクトのクラス」を確認し、何のメッセージを「そのオブジェクト」に送るかを決めるものでした。 このパターンを、未発見のダックタイプを見つけるために使いました。ここでは同じパターンによって、未発見のサブタイプ、よく知られた言い方をすればサブクラスの存在が示されています。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3236-3240). Kindle 版.
継承を選択する
オブジェクトはメッセージを受け取ります。コードがどれだけ複雑であろうとも、受け取るオブジェクトは、最終的には2つの方法のうちどちらかで処理します。直接応答するか、応答してもらうべくほかのオブジェクトにメッセージを渡すかのどちらかです。継承によって、2つのオブジェクトが関係を持つように定義できます。その関係によって、1つ目のオブジェクトがメッセージを受け取り、それが理解できないものだった場合、自動的に転送、つまり委譲を行い、そのメッセージを2つ目のオブジェクトに渡せるようになります。それだけのことです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3266-3271). Kindle 版.
多くのオブジェクト指向言語は、「単一継承」のみをできるようにすることで、この複雑さを回避しています。単一継承では、サブクラスは親となるスーパークラスを1つしか持つことができません。Rubyもそのようにしています。つまり、Rubyは単一継承です。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3278-3281). Kindle 版.
サブクラスはそのスーパークラスを特化したものと言える
継承関係を描く
- 接続線は、クラスが関係していることを示している
- 中空の三角形は、関係が継承であることを意味する
- 三角形の指す先は、スーパークラスを含む箱に接続される
上記は図の読み方についてまとめたものです。
6.3継承を不適切に適用する
上記の図では、Bicycle
クラス内にRoadBike
が入っているでの、MountainBike
クラスを作成するとRoadBike
の不要な情報まで継承されてしまう。
Bicycle
クラスをRoadBike
とMountainBike
の抽象クラスにする必要がある
6.4抽象を見つける
抽象的なスーパークラスをつくる
これが最終的な目標です。つまり、この継承構造こそまさに現在つくろうとしているものです。Bicycleが共通の振る舞いを持ち、MountainBikeとRoadBikeがそれぞれ特化した振る舞いを追加します。Bicycleのパブリックインターフェースはsparesとsizeを含まなければなりません。そしてそのサブクラスは、それぞれ個々の部品を追加します。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3414-3418). Kindle 版.
イテレーションとは、一連の工程を短期間で繰り返す、開発サイクルのことです。 短いスパンで開発手法を見直す「アジャイル開発」の一概念として知られています。 設計、開発、テスト、改善のサイクルを短期間で繰り返すため、問題の発見や改善が容易になります。
抽象的な振る舞いを昇格する
リファクタリングの戦略を決めるときのみならず、設計の戦略を決める際、一般的に有用であるのは、「もし間違っているとすれば、何が起こるだろう」と質問することです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3555). Kindle 版.
一般に、新たな継承の階層構造へとリファクタリングをする際は、抽象を昇格できるようにコードを構成すべきであり、具象を降格するような構成にはすべきではない
- 設計者の決断の際の2つのコスト
- 実装コスト
- 変更コスト
このコストの両方を考慮に入れて代替案を選ぶことによって、変更コストを最小化する慎重な選択するようになる
テンプレートメソッドパターンを使う
- テンプレートメソッドとは
- オーバーライドすることによって何かに特化できる機会をサブクラスに与えること
- スーパークラス内で基本の構造を定義し、サブクラス固有の貢献を得るためにメッセージを送ること
以下は、現段階でのテンプレートメソッドの定義です(完成形ではありません)
class Bicycle attr_render :size, :chain, :tire_size def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size end def default_chain # <- 共通の初期値 '10-speed' end end class RoadBike < Bicycle # ... def default_tire_size # <- サブクラスの初期値 '23' end end class MountainBike < Bicycle # ... def default_tire_size # <- サブクラスの初期値 '2.1' end end
すべてのテンプレートメソッドを実装する
class Bicycle # ... def default_tire_size raise NotImplementedError, "This #{self.class} cannot respond to:" end end
上記のように文書化すると未来にたしてわかりやすくエラー(例外)を伝えることができる
親クラスに定義されておらず、各サブクラスにメソッドが定義されている場合は、テンプレートメソッドを準備するとよい
テンプレートメソッドの要件は、有用なエラーを発生させる、合致するメソッドを実装することで常に文書化しましょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3733-3734). Kindle 版.
6.5スーパークラスとサブクラス間の結合度を管理する
かんたんで明白なものと、より複雑ではあるけれど堅牢なものがある
結合度を理解する
以下のようなコードの場合は、super
という記述をするということに関して、依存しすぎているためエラーの原因になりうる。かつこれらを実装しようとするプログラマーは、事前の知識を知らないので、迷路に迷い込んでしまう。
(以下の結果では、エラーが発生しないが
nil
が返るようになっており、より気づけない設計となっている)
class RecumbentBike < Bicycle attr_reader :flag def initialize(args) # 'super'を送信するのを忘れた @flag = args[:flag] end def spares super.merge({flag: flag}) end def default_chain '9-speed' end def default_tire_size '28' end end bent = RecumbentBike.new(flag: "tall and orenge") bent.spares # -> {:tire_size => nil, <- 初期化されていない # :chain => nil # :flag => "tall and orenge"}
フックメッセージを使ってサブクラスを疎結合にする
以下はテンプレートメソッドを使用した実装パターンです。
前節の結合度を理解する
で触れた依存関係を取り除き、階層構造の各層間の結合度が低減し、変更にも強くなるコードになります。
class Bicycle def initialize(args={}) @size = args[:size] @chain = args[:chain] || default_chain @tire_size = args[:tire_size] || default_tire_size post_initialize(args) end def post_initialize(args) nil end end class RoadBike < Bicycle def post_initialize(args) #... @tape_color = arg[:tape_color] end end
6.6まとめ
継承は、関連する型に関する問題を解決します。ここでの関連する型とは、山ほど共通の振る舞いを持つものの、いくつかの面においては相違もある型です。継承によって、共有されるコードを隔離でき、そして共通のアルゴリズムを抽象クラスに実装できるようになります。しかも同時に、継承は抽象クラスの特化をサブクラスができるような構造を提供します。抽象的なスーパークラスをつくるための一番良い方法は、具象的なサブクラスからコードを押し上げることです。正しい抽象を特定するのが最もかんたんなのは、存在する具象クラスが少なくとも3つあるときです。この章の単純な例では2つにしか頼りませんでしたが、現実には、3つの事例によって追加される情報を待ったほうが、より良いでしょう。抽象スーパークラスは、テンプレートメソッドパターンを使うことで、その継承者に専門的に特化するよう促します。そしてフックメソッドを使うことで、superの送信を強制せずとも継承者がスーパークラスに特化を提供できるようにします。フックメソッドによって、抽象的なアルゴリズムを知らなくともサブクラスは専門性を加えられるようになります。サブクラスがsuperを送る必要性が取り除かれるので、階層構造の各層間の結合度が低減し、変更にも強くなります。適切に設計された継承の階層構造は、たとえアプリケーションについてあまり詳しくないプログラマーでもあっても、新たなサブクラスを用いてかんたんに拡張できます。この拡張の容易さが、継承の最も大きな強みです。もし、現在抱えている問題が、安定した、共通の抽象のいくつもの専門性を必要とするものであれば、継承はきわめてコストの低い解決法となるでしょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3963-3978). Kindle 版.
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第5章ダックタイピングでコストを削減する-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第5章ダックタイピングでコストを削減する
- はじめに
オブジェクト指向設計の目的は変更にかかるコストを下げることです。メッセージが設計の中心にあることはもうご存じのことだと思います。また、いまとなっては、厳密に定義されたパブリックインターフェースを構築したくなっていることでしょう。この2つの考えを組み合わせることで、強力な設計テクニックを得られます。それにより、コストをさらに削減できるでしょう。このテクニックは「ダックタイピング」として知られています。ダックタイプはいかなる特定のクラスとも結びつかないパブリックインターフェースです。クラスをまたぐインターフェースは、アプリケーションに大きな柔軟性をもたらします。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2634). Kindle 版.
ダックタイプによって型付けされたオブジェクトは、さながらカメレオンのようです。
目的はもちろん、アプリケーションの柔軟性と変更のしやすさを高めることです。
5.1ダックタイピングを理解する
プログラミング言語での「型(type)」という用語は、変数の中身の分類を示すために使われます。手続き型言語では、少数の、決まった数の型があらかじめ用意されていて、一般的に「データ」の種類を記述するために用いられます。変数の中身の分類についての知識、つまり型についての知識があれば、アプリケーションはその中身の振る舞いを予想できます。アプリケーションは、数値は数式の中で利用できる、文字列は結合できる、配列は添字でアクセスできる、というようなことを、上手に仮定できます。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2646). Kindle 版.
オブジェクト指向設計において重要な事(再確認)
- クラスは、オブジェクトがパブリックインターフェースを獲得するための1つの方法でしかない
- 重要なのは、オブジェクトが何で「ある」かではなく、何を「する」か
ダックタイプの重要性
- クラスをまたいだ型を認識すること、そしてそのパブリックインターフェースを、「第4章柔軟なインターフェースをつくる」でつくったクラス内の型のものと同じくらい、意図を持って入念につくること
- つまりダックタイプのパブリックインターフェースは、契約を表すものです。この契約は明示的で、適切に文書化されたものでなければならない
5章で検討すること(想定)
ダックタイプを説明する最善の方法は、ダックタイプを使わない場合どうなるかを検討することです。
ダックを見逃す
このprepareメソッドは、Mechanicクラスに対しての明示的な依存こそしていませんが、prepare_bicyclesに応答できるオブジェクトを受け取る、ということには依存しています。この依存はとても基本的なことなので、かんたんに見落としやすく、軽視してしまいがちですが、それでも存在することには変わりありません。Tripのprepareメソッドは、この引数が自転車を準備してくれるものを持っていると、固く信じています。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2695-2700). Kindle 版.
問題を悪化させる
- 現状の問題点
- prepareメソッド内の新たな依存を数えてみましょう。このメソッドは特定のクラスに依存していて、ほかのクラスは役に立たない
- これらのクラスの具体的なメソッド名に依存しており、それぞれのクラスが理解するメッセージの名前を、それが必要とする引数と共に知っている
- この知識はすべてリスクを上げるものです。たくさんのかすかな変更が、このコードに副作用をもたらすことがある
- この手のコードは増殖してことがある傾向にある
ダックを見つける
依存を取り除くための鍵となるのは、「Tripのprepareメソッドは単一の目的を果たすためにあるので、その引数も単一の目的を共に達成するために渡されてくるということを認識すること」です。どの引数も同じ理由のためにここに存在し、その理由自体は引数の背後にあるクラスとは関係しません。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2771-2774). Kindle 版.
この想定によって、形勢はきれいに一変します。既存のクラスから思考を解き放ち、ダックタイプを生み出すことができたのです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2783-2784). Kindle 版.
以下に新しい設計のコードを載せます。prepareメソッドは、引数が複数のPreparerであることを想定するようになっています。また、それぞれの引数のクラスは、新しいインターフェースを実装しています。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2800-2802). Kindle 版.
# 旅行の準備はより簡単になる class Trip attr_render :bicysles, :customaers, :vehicle def prepare(preparers) preparers.each {|preparer| preparer.parepare_trip(self)} end end # 全ての準備者(Preparer)は # 'prepare_trip'に応答するダック class Mechanic def prepare_trip(trip) trip.bicycles.each {|bicycle| prepare_bicycle(bicycle)} end # ... end class TripCoordinator def prepare_trip(trip) buy_food(trip.customers) end # ... end class Driver def prepare_trip(trip) vehicle = trip.vehicle gas_up(vehicle) fill_water_tank(vehicle) end # ... end # Sandi Metz. オブジェクト指向設計実践ガイド # ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
このprepareメソッドは、新しいPreparerを受け入れる際に、変更が強制されることはありません。また、必要に応じて追加のPreparerをつくるのもかんたんです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2816-2819). Kindle 版.
ダックタイピングの影響
-
具象性
- コードを理解しやすくしてくれる
- 拡張する際に注意が必要
- 上記の理由から拡張の際のコストがかかる
抽象性
- コードを理解するのが難しい
- コードを理解してしまえば、拡張するのが容易である
- 上記の理由から拡張の際のコストを抑えられる
ダックタイプとは
オブジェクトを、そのクラスではなくあたかも振る舞いによって定義されているかのように扱えるようになれば、表現力と柔軟性を備えた設計という、新たな領域に足を踏み入れたも同然です。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2836). Kindle 版.
Column - ポリモーフィズム -
一般的な定義
- 「Morph」とは、ギリシャ語でform、つまり形態を意味する
- 「morphism」は、形態を持っている状態
- 「polymorphism」とは、多数の形態を持っている状態
-
- 多岐にわたるオブジェクトが、同じメッセージに応答できる能力を指す
- メッセージの送り手は、受け手のクラスを気にする必要がなく、受け手は、それぞれが独自化した振る舞いを提供する
ポリモーフィズムを使うとき、すべてのオブジェクトが適切に振る舞うことを確実にするのは、プログラマーです。このことについては、「第7章モジュールでロールの振る舞いを共有する」にて扱います。
5.2ダックを信頼するコードを書く
設計上で難しいことは、ダックタイプが必要であることに気づくことと、そのインターフェースを抽象化することです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2871). Kindle 版.
隠れたダックを認識する
クラスで分岐するcase文
- クラスで分岐するcase文の場合は、何か共通のものを共有しているに違いないとわかります。
- 上記に気づく方法
コードを調べ、「prepareがその引数のそれぞれから望むものは何だろうか」と、自身に問いかけましょう。その問いへの答えに、送るべきメッセージが示されます。このメッセージから、根底にあるダックタイプが定義されはじめます。
kind_of?とis_a?
- responds_to?
ダックを信頼する
デメテルの法則を違反するのと同じように、この形式のコードは何かオブジェクトを見逃していることを示します。まだパブリックインターフェースを発見できていないオブジェクトを見逃しているのです。見逃しているオブジェクトが、具象クラスではなくダックタイプであるという事実は、まったく重要ではありません。重要なのはそのインターフェースであり、インターフェースを実装するオブジェクトのクラスではないのです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2928-2929). Kindle 版.
上記の引用の要約
- パプリックインターフェイス(メソッドの集合(メッセージ))からなるオブジェクト(クラス)を見逃している
- 見逃しているオブジェクトは、クラスかダックタイプ(オブジェクトを、そのクラスではなくあたかも振る舞いによって定義されているかのように扱えるメソッド的な集合?)かは重要でない
- 重要なのは、そのインターフェース(メッセージ)であり、インターフェースを実装するオブジェクト(クラス自身)ではない
問題のあるコードが想定していることに集中し、その想定をダックタイプを見つけるために使いましょう。ダックタイプをつかめたら、そのインターフェースを定義し、必要なところで実装します。実装したら、それが正しく振る舞ってくれると信頼しましょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2934-2936). Kindle 版.
上記の引用の要約
- ダックタイプ(ポリモーフィズム)
- インターフェイスを定義すること(ポリモーフィズム的なものから依存関係を明確にして単一責任にすること)
- 必要なところに実装する(オブジェクト化(クラス化)することであったり、ダックタイピングで実装する)
ダックタイプを文書化する
最も単純なダックタイプは、単にパブリックインターフェースの取り決めとしてだけ存在するものです。
このPreparerダックタイプとそのパブリックインターフェースは、設計上では具象的な部分ですが、コード上では仮想的な部分です。Preparerは抽象的です。抽象的であるというのは、設計の道具としてはPreparerに強さを与えますが、まさにその抽象性が、ダックタイプをコード上では明白とは言えないようなものにしてしまいます。ダックタイプをつくるときは、そのパブリックインターフェースの文書化とテストを、両方ともしなければなりません。幸い、優れたテストは最高の文書でもあります。ですから、すでに半分は終わっているようなものでしょう。あとはテストを書きさえすれば、両方とも終わりです。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2944). Kindle 版.
ダック間でコードを共有する
ダックタイプを実装するクラスは振る舞いもいくらか共有する必要があるときがたびたびあることに気づきます。コードを共有するダックタイプを書くことについては、第7章で取り扱う。
賢くダックを選ぶ
- ダックタイプにもしなくていいパターンがある
- Rubyが提供しているコアクラスを使用して、クラス間を跨いだ処理の場合は、変更を強制するかたちで変わる可能性は、極端に低いから実装(そのまま)にしても良い
理由は開発のコスト(可能性)を下げることが目的だから
- Rubyが提供しているコアクラスを使用して、クラス間を跨いだ処理の場合は、変更を強制するかたちで変わる可能性は、極端に低いから実装(そのまま)にしても良い
余談ですが、コアクラスを変更できるモンキーパッチという方法があるが、これをうまく使用することは大切。だが、慣れないうちは変更することが危険であるので、その正当性を説明できるような場合のみ変更を加える
5.3ダックタイピングへの恐れを克服する
5.3では動的型付けと静的型付けの比較をしていく
静的型付けによるダックタイプの無効化
静的型付け言語は、それぞれの変数の型と、メソッドのすべてのパラメーターを明示的に宣言することを求めます。動的型付け言語は、この要求を省きます。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3002). Kindle 版.
- 静的型付けの信念
- コード内でオブジェクトのクラスを検査しがちである
- 上記によって、型付けに縛られ、ダックタイプの利用を不可能にしてしまう
ダックタイプは、コードが安全に依存できる安定した抽象を明らかにする
静的型付けと動的型付け
静的型付け言語の特性
静的型付け言語の強み
動的型付け言語の特性
動的型付け言語の強み
メタプログラミング(Metaprogramming)とは、コンピュータプログラムそのものをデータのように扱えるプログラミング技術を意味している。 プログラムによって新たなプログラムを生成し、既存プログラムを閲覧・分析・修正・変形・拡張できることがメタプログラミングである。
動的型付けを受け入れる
一定のアプリケーションにおいては、動的型付けでの実装よりも、上手に最適化された静的型付けのコードのほうが優れた性能を発揮します。動的型付けのアプリケーションにおいて、頑張っても十分な実行速度を実現できないのであれば、静的型付けが代案となります。どうしても実装速度を上げたいのであれば、静的型付けにするしかないでしょう。
文書としての型宣言の価値についての議論は、もっと主観的です。動的型付けの経験が豊富な人にとっては、型宣言は気を散らす存在です。静的型付けに慣れている人にとっては、型情報がないと混乱するかもしれません。静的型付け言語(たとえばJavaやC++)から入って、Rubyに明示的な型宣言がないことで港に繋がれた船の綱を外されたように感じる人は、なんとか持ちこたえてください。いくつもの事例証拠があります。一度慣れてしまえば、煩雑さの少ないこちらの構文のほうが読みやすく、書きやすく、理解しやすいと思うでしょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3036-3039). Kindle 版.
- 静的型付け言語はパフォーマンスの部分では確かに有用である
静的型付けの前提
- コンパイラは不慮の型エラーから本当に救って「くれる」
- コンパイラの助けなしでは、これらの型エラーは「起こる」
コンパイラはコンパイルを実行するだけで、型エラーを防ぐのはプログラマー自身の知力に任されています。コードの良さは結局テストの良さになります。実行時のエラーは依然起こり得ます。静的型付けによって安全になるという考えは、確かに心地良いかもしれませんが、幻想です。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3067-3070). Kindle 版.
静的型付け言語は、ダックタイプが使えないという欠点がある
ダックタイピングは、動的型付けの上に成り立ちます。ダックタイピングを使うためには、この動的さを受け入れる必要がある
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3079-3081). Kindle 版.
5.4まとめ
メッセージこそが、オブジェクト指向アプリケーションの中心にあるものです。メッセージはパブリックインターフェースを介し、オブジェクト間で交換されます。ダックタイピングは、これらのパブリックインターフェースを特定のクラスから切り離し、何であるかではなく何をするかによって定義される、仮想の型をつくります。ダックタイピングは、根底にある抽象をあらわにします。ダックタイピングを使わなければおそらく不可視だったものです。これらの抽象に依存することによって、リスクは低減され、柔軟性は高まります。アプリケーションのメンテナンスコストは下がり、変更もよりかんたんになります。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.3081-3087). Kindle 版.
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第4章 柔軟なインターフェースをつくる-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第4章 柔軟なインターフェースをつくる
オブジェクト指向アプリケーションは単なるクラスの集まりではなく、メッセージによって定義されている
- オブジェクト間で受け渡されるメッセージについて考慮すべきこと
- オブジェクトの目的
- オブジェクトの依存関係
- オブジェクトが互いに、どのようなやりとりをするか
4.1インターフェースを理解する
1つ目(左)のアプリケーション
1つ目のアプリケーションでは
- メッセージに明らかなパターンはない
- すべてのオブジェクトは、任意のメッセージを任意のオブジェクトに送れるようになっている
1つ目のアプリケーションのオブジェクトでは
- 再利用が困難
- それぞれが自身について外部に晒しすぎている
- 近隣のオブジェクトについても知りすぎている
どのオブジェクトの、どのメソッドであっても、その粒度にかかわらずほかのオブジェクトから実行できることが問題である。
2つ目(右)のアプリケーション
2つ目のアプリケーションでは
- メッセージには明確に定義されたパターンがある
- オブジェクトは明確で適切に定義された方法でコミュニケーションをとっている
2つ目のアプリケーションのオブジェクトでは
- 着脱可能
- コンポーネントのようなオブジェクトから構成されている
- それぞれが互いに最小のことのみを明らかにしている
- 互いについて知っていることもできる限り最小限である
上記のアプリケーションでは
- 制約があることは明らか
- どのメッセージがどのオブジェクトに渡せるのかについて、何らかの合意と約束ある
- それぞれのオブジェクトは、ほかのオブジェクトが使えるようなメソッドを明確に定義できている
答えは、クラスが何を「する」かではなく、何を「明らかにする」かにあります。
4.2インターフェースを定義する
パブリックインターフェースとは
- クラスの主要な責任を明らかにする
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化されている
プライベートインターフェースとは
- 実装の詳細に関わる
- ほかのオブジェクトから送られてくることは想定されていない
- どんな理由でも変更され得る
- 他者がそこに依存するのは危険
- テストでは、言及さえされないこともある
責任、依存関係、そしてインターフェース
- 単一の責任について
- 単一の目的である
- クラスの責任は、おのずとその目的を果たすこと
依存関係について
- クラスは自身より変更の可能性が低いクラスにのみ依存するべき
パブリックとプライベートについて
- メソッドにパブリックやプライベートと印を付けることは、クラスの使用者に対し、どのメソッドには安全に依存できそうか、ということを伝えている
- パブリックメソッドを使うときは、それらのメソッドが安定していると信頼していることである
- プライベートメソッドに依存することを決めるときには、本質的に不安定な何かに依存していることを理解し、それゆえ離れたところの、関係のない変更にも影響を受けるリスクが増えることも知ったうえで行う必要がある
4.3パブリックインターフェースを見つける
パブリックインターフェースを見つけ、定義することは1つの技巧である
- ただ、これが設計上難しいのは、はっきりと決められたルールがないからである
- 「十分に良い」インターフェースをつくる方法はいくらでもあるが、「十分に良くない」インターフェースのコストはすぐに明らかになるわけでもないので、失敗から学ぶのもかんたんではない
オブジェクト指向設計とは 設計の目標は、例に漏れず、今日の要求に応えるために十分なコードだけを書きつつ、将来的な柔軟性を最大限に保つこと
見当をつける
最初はテストからはじめるべきだと強く思っていることでしょう。しかし、強く思ったからといってかんたんになるわけではありません。多くの設計初心者にとって、最初のテストを考えることは本当に難しいものです。最初のテストを書くためには、何をテストしたいかについての考えが必要です。
ドメインオブジェクトとは
- アプリケーションにおいて「データ」と「振る舞い」の両方を兼ね備えた「名詞」を表す
- 大きくて目に見える現実世界のものを表し、かつ最終的にデータベースに表されるものと言える
- こだわりすぎると、無理な振る舞いをさせがち
より良い設計をする為に
- オブジェクトではなく、オブジェクト間で交わされるメッセージに注意を向ける必要がある
- これらのメッセージが、ドメインオブジェクト同様に必要なものの、はるかに見えにくいほかのオブジェクトを見つけるための手がかりとなってくれる為である
シーケンス図を使う
シーケンス図とは
- 上記のような図のことをシーケンス図という(一例にすぎません)
- 「オブジェクト」とその間で交わされる「メッセージ」のフローのこと
シーケンス図のメリット
- さまざまなオブジェクトの構成やメッセージ受け渡しの体系を簡潔に実験できる
- 思考を明確にし、ほかの人と共同作業したり意思疎通したりするための手段となる
公式の様式から外れたからといって、責められることはない
- 基本的な設計の質問
- 「このクラスが必要なのは知っているけれど、これは何をすべきなんだろう」から「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」へ変えることが重要である
- オブジェクトが存在するからメッセージを送るのではありません。メッセージを送るためにオブジェクトは存在する
「どのように」を伝えるのではなく「何を」を頼む
送り手の望みを頼むメッセージと、受け手にどのように振る舞うかを伝えるメッセージの違いはささいなものに見えるかもしれません。しかし、その影響は絶大です。
適切に定義されたパブリックインターフェースを持つ、再利用可能なクラスをつくるための鍵となる部分です。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2206-2207). Kindle 版.
- パブリックインターフェイス(振る舞い)を小さくする
- コードのメンテナンス性の向上
- コードの柔軟性の向上
コンテキストの独立を模索する
- 考えられる限り最も良い状況は
- オブジェクトがそのコンテキストから完全に独立していること
- 依存オブジェクトの注入で解決できる
ほかのオブジェクトを信頼する
- 図4.5では、TripはMechanicに「私は自分が何を望んでいるかを知っているし、あなたがそれをどのようにやるかも知っているよ」と伝えている
- 図4.6では、「私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ」と伝えている
- 図4.7では、「私は自分が何を望んでいるかを知っているし、あなたがあなたの担当部分をやってくれると信じているよ」と伝えている
この(図4.7)手放しの信頼が、オブジェクト指向設計の要です。これによって、オブジェクトは自身をコンテキストに縛り付けることなく、共同作業できるようになります。また、これは成長し、変化することが期待されるアプリケーションには必要不可欠です。
オブジェクトを見つけるためにメッセージを使う
TripFinderクラスが含まれるべきでしょう。図4.8が示すシーケンス図では、適切な旅行(suitabletrips)を見つける責任を、TripFinderが負っています。TripFinderは、何がどうなれば適切な旅行になるかの知識をすべて持っています。ルールを知っています。TripFinderの仕事は、このメッセージに応答するために必要なことなら何でも行うことです。TripFinderは、安定したパブリックインターフェースを提供し、また、変化しやすく乱雑な内部実装の詳細は隠しています。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2379-2384). Kindle 版.
まとめ
- メッセージから、各クラス毎に関係性の低いメソッドを隔離する為の新たなクラスを見出す
- 結果として、パブリックインターフェースが安定し、変化しやすく内製的な設計にすることができる
クラスが単一責任かどうかを見極める
以下のような方法がある。
- あたかもそれに知覚があるかのように仮定して問いただすこと
- クラス(主語)でメソッド(機能)が成り立っているか質問する
- 最小で有用なことは良いクラスである。
- 1文でクラスを説明してみること
4.4一番良い面(インターフェース)を表に出すコードを書く
適切に定義されたインターフェースが存在することは、それが完璧であることよりも重要である
明示的なインターフェースをつくる
設計者の目的
- 今の役割を果たすこと
- かんたんに再利用できる
- 将来的な予期せぬ用途にも対応できるコードを書くこと
「パブリック」インターフェースに含まれるメソッドのポイント
- 明示的にパブリックインターフェースだと特定できる
- 「どのように」よりも、「何を」になっている
- 名前は、考えられる限り、変わり得ないものである
- オプション引数として、ハッシュをとる
Rubyには、関連するキーワードが3つあります。publicにprotected、そしてprivateです。これらのキーワードは、まったく異なる2つの用途で用いられます。
- 1つ目は、どのメソッドが安定でどのメソッドが不安定かを示す
- 2つ目は、アプリケーションのほかのところに、どれだけメソッドを見える化を制御する
public等のキーワードを使うことのメリット
- 「将来の」プログラマーが持つ情報よりも、いまの自分のほうがより良い情報を持っていると信じている
- いまの自分が不安定だと考えているメソッドを、将来のプログラマーに不用意に使われることは防がなければならないと信じている
これは明らかに安定だろうというメソッドでも、定期的に変わることもあります。逆に、最初の段階では一番不安定だったメソッドでも、時間という試練を超えて存在し続けることもあります。制御できるという幻想が快適であるなら、気にせずキーワードを使ってください。しかし、何人ものかなり実力のあるRubyプログラマーが、あえてキーワードを省略し、代わりにコメントやそれ用の命名規則を使い、インターフェースの「パブリック」と「プライベート」な部分を示しています(たとえばRubyonRailsでは、プライベートメソッドの先頭に_を付けます)。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2456-2462). Kindle 版.
まとめ
- メンテナンスの方向性を示すもの(現段階での解釈)
ほかのパブリックインターフェースに敬意を払う
彼らは、時空を超えてどのメソッドになら依存可能なのかを、必死に伝えようとしているのです。彼らが付けたpublicとprivateの違いは、「いまの自分」を助けようと意図されたものであり、それに真剣に耳を傾けることに越したことはないでしょう。 もし自身の設計が、ほかのクラスにあるプライベートメソッドの使用を強制するものである場合、まずはその設計を再考しましょう。努力を注ぐことによって、代案を発見することは可能です。代案を見つけるために懸命に努力すべきでしょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2487-2488). Kindle 版.
外部フレームワークへの依存は、技術的負債の一形態になる可能性があるので、そのような依存は避けるべきである
プライベートインターフェースに依存するときは、注意深く
プライベートメソッドの使用を避けられないとしても、アプリケーションの多くの場所で参照されることは防げます。プライベートインターフェースへの依存は、リスクを高めます。依存を隔離することによって、このリスクを最小限にとどめましょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2485-2487). Kindle 版.
コンテキストを最小限にする
パブリックインターフェースを構築する際は、そのパブリックインターフェースが他者に要求するコンテキストが最小限になることを目指す
- 「何を(what)」と「どのように(how)」の違いを常に念頭に置き、パブリックメソッドをつくる際は、メッセージの送り手が、クラスがどのようにその振る舞いを実装しているかを知ることなく、求めているものを得られるようする
- 最初に書いた人がパブリックインターフェースを定義しなかったからといって、遅すぎることない、自身の手でパブリックインターフェースをつくることも遅くない
4.5デメテルの法則
デメテルを定義する
デメテルとは
- メッセージを「送る」ことができるオブジェクトの集合を制限する
- 3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介すことを禁じる
- 「直接の隣人にのみ話しかけよう」だとか、「ドットは1つしか使わないようにしよう」なんて言い方もされる
デメテルの法則を考える為の具体例
デメテルの法則のポイント
- .(ドット)を使用してメソッドチェーンをつなげることは場合によっては良いとされる
- 複数のクラス(オブジェクト)をメソッドチェーンでつなげることはデメテルの法則に違反する
法則を違反することによる影響
デメテルの法則が「法則」になったのは、人間がそう決めたからです。この大げさな名前にだまされてはいけません。法則といっても、これは「毎日歯を磨くべきです」といった規則のようなもので、万有引力の法則とは異なります。歯医者には言いにくいかもしれませんが、たまに破ったところで、世界が崩壊することはありません。第2章では、コードは「見通しが良い(transparent)」もの、「合理的(reasonable)」なもので、「利用性が高い(usable)」もの、「模範的(exemplary)」なもの(TRUE)であるべきだと述べました。上記のメッセージチェーンには、これにかなっていないものがあります。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2526-2532). Kindle 版.
違反を回避する
- 違反を回避する方法
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2580). Kindle 版.
デメテルに耳を傾ける
デメテルに耳を傾ける、ということは、自身の視点に注意を払うということです。
4.6まとめ
メッセージに集中することによって、見過ごされていたオブジェクトが明らかになるかもしれません。メッセージが、受け手がどのように振る舞うかを伝えるものではなく、相手を「信頼」し送り手が望むことを頼むものであるとき、オブジェクトは予期しなかった方法や、まったく新しい方法で再利用できる、柔軟なパブリックインターフェースを自然に進化させていくでしょう。 くでしょう。
Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.2626). Kindle 版.
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第3章 依存関係を管理する-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第3章依存関係を管理する
オブジェクトに望まれる振る舞い
- オブジェクト自身が知っている
- 継承している
- メッセージを理解するほかのオブジェクトを知っている
ここで言う振る舞いは、クラス自身が独自に実装すべき振る舞いです。 2つ目の、振る舞いの継承については「第6章継承によって振る舞いを獲得する」で扱います。 この章では、3つ目の、振る舞いがほかのオブジェクトに実装されているときに、それにアクセスすることについて論じましょう。
3.1依存関係を理解する
一方のオブジェクトに変更を加えたとき、他方のオブジェクトも変更せざるを得ないおそれがあるならば、片方に依存しているオブジェクトがあります。
依存関係を認識する
以下の時に依存関係があると考える。
- ほかのクラスの名前(名前から連想される他のクラスがある時)
- そのクラスの中に他のクラスのメソッドがあることがわかる時
- メッセージが要求する引数とそれら引数の順番を考える時
上記があるときは、不必要な依存を生む。これが元に合理性を損なう。
上記はない方がいい。
オブジェクト間の結合(CBO:CouplingBetweenObjects)
依存関係は管理が難しい
- 1つのエンティティが依存関係に寄って、さも複数のエンティティかのような管理が求められる
この場合、管理が難しくなるのは時間の問題
ほかの依存関係
ほかにも、依存関係に関連した一般的な問題がある
破壊的なたぐいの依存が生じる場合
- 「『何かを知るオブジェクト』を知るオブジェクト」を知るオブジェクトがあるとき、つまり、いくつものメッセージをチェーンのように繋いで、遠くのオブジェクトに存在する振る舞いを実行しようという場合に生じます。
6章で詳しく
- 「『何かを知るオブジェクト』を知るオブジェクト」を知るオブジェクトがあるとき、つまり、いくつものメッセージをチェーンのように繋いで、遠くのオブジェクトに存在する振る舞いを実行しようという場合に生じます。
大別できる依存関係
- テストがコードに依存するとコードが毎回壊れる(テストがコードに依存している)
9章で詳しく
- テストがコードに依存するとコードが毎回壊れる(テストがコードに依存している)
3.2疎結合なコードを書く
依存関係をより詳細に理解することが大事
- 依存関係を把握し、必要のない依存関係を認識し、不要な依存関係を取り除く
疎結合とは
独立・孤立
依存オブジェクトの注入
プロジェクト全体の名前の修正
- そんなに重大ではない。まだ変更は容易
ハードコーディングとは、本来プログラム中に記述すべきでないリソース(エラーメッセージなど)を、直接ソースコード中に埋め込むことである。
- クラス内のクラスは隔離し、引数で値を拾ってくる方が良い
- 依存オブジェクトの注入を使ってコードを整えることができるかどうかは、設計者が持つ次の能力に左右される。
- クラス名を知っておく責任
- そのクラスに送るメソッドの名前を知っておく責任
- どこかほかのクラスに属するものなのではないかと疑える能力
- 依存オブジェクトの注入を使ってコードを整えることができるかどうかは、設計者が持つ次の能力に左右される。
依存を隔離する
最も理想的なのは、不必要な依存はすべて取り去ってしまうことしかし、これは「技術的には」可能であるものの、現実にはおそらく不可能でしょう。すでに存在するアプリケーションに取り組んでいると、実際にどれだけ変更できるかには厳しい制約がある、とわかることがあります。コードを完璧にできないのであれば、その次に目標とすべきは現状の改善です。完璧さは手に入れられないとしても、最初に見つけたときよりもコードをより良いものにしていくことによって、全体の状況を改善することはできます。
依存は、クラスの単一責任的な感じで隔離するべき
インスタンス変数の作成を分離する
- initializeメソッドで隔離する
- メソッド化して、独自に明示的に定義する
依存関係を隠蔽するのではなく、明示的にする
脆い外部メッセージを隔離する
外部的な依存を取り除き、専用のメソッド内にカプセル化するのが大事
- 引き算的な考えが大事
引数の順番の依存を取り除く
- 引数の渡す順番は、初期段階では足したり引いたり、デフォルト値を設定したりとサイクルを繰り返すこともあり、固定化された順番の引数を使うとそのサイクルを回すたびに修正することがある。
- これらが残ったまま大規模化すると、引数に変更を加えること自体を避けるようになる。
初期化の際の引数にハッシュを使う
ハッシュを受け取るようにコードを変えると良い。以下ハッシュのメリット
- 順番に依存せず、名前(キー)で値(バリュー)を扱える
- 名前で管理できている事で、メンテナンス性も上がる
- 固定順とハッシュの組み合わせも併用でき、扱いやすい
明示的にデフォルト値を設定する
args[:boolen_thing] || true
明示的なデフォルト値を設定する方法は以下
- ||メソッドを使用する
- nilやfalseを使用したい場合はfetchメソッドを使用する
- さらに複雑なデフォルト値を設定したい場合はmergeメソッドを使用して、メソッド化する
複数のパラメーターを用いた初期化を隔離する
複数のパラメーターを用いた初期化を隔離する方法は以下
- 他のオブジェクトを作成する事が目的のオブジェクトはファクトリーである
- モジュール化することで、作り手の意図も伝わる
- 固定順の引数をオプションハッシュに置き換えるのは、自分で変更がきかない外部のインターフェースに依存せざるを得ない場合に適している
3.3依存方向の管理
依存関係には方向があり、逆にしたら良い
依存関係の逆転
依存関係の方向が、未来に生じるであろう変更に大きな影響がある
依存方向の選択
少しの間、クラスがあたかも人間であるかのように考えてみましょう。彼らの振る舞い方にアドバイスをするとすれば、きっと、「自身より変更されないものに依存しなさい」とアドバイスをするのではないでしょうか。
依存方向を考える上で、考慮すべき点
- あるクラスは、ほかのクラスよりも要件が変わりやすい
- 具象クラスは、抽象クラスよりも変わる可能性が高い
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ
変更の起きやすさを理解する
変更の置きやすさに重心を置く
- 変更の頻度を、順位付けして管理することが大事
具象と抽象を認識する
- コードの具象性と抽象性を考えるのが大事
- 静的型付け言語では、型付けの際に意図が見える
- 動的でも、設計の際には「インターフェース」を考慮すべき
インターフェースとは、型付けのことではないか(問い)
大量に依存されたクラスを避ける
大きいクラスとの依存関係をなくすことが大事
問題となる依存関係を見つける
A:クラスが抽象になるから依存関係が増える
B:中立的である
C:中立的である
D:悪魔のような関係
依存関係を減らすことが何より大切である
- 「自分より変更されないものに依存しなさい」
3.4まとめ
依存関係の管理は、将来のアプリの核となる
- 特に、鍵は依存関係の方向を制御すること
- 自身より変更の少ないクラスに依存させること
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon
<輪読会> オブジェクト指向実践ガイド -第2章 単一責任のクラスを設計する-
輪読会メンバー
- Izumi Haruya
- Sekine Yutaro
第2章 単一責任のクラスを設計する
単一責任のクラスをどのように扱うべきか
- クラスが「シンプルであれと主張すること」が大切である
- クラスは「いますぐに」求められる動作を行い、後にも簡単に変更できるモデル化をする
前者は簡単、ただ後者を考慮すると難しい
2.1 クラスに属するものを決める
- 問題は技術的な知識(コード)より構造(クラスに属するものの配置)が大切
メソッドをグループに分けクラスにまとめる
- クラスの設計が大切である
- メソッドが増えていくとグループ分けしてクラスにまとめる
ここのメソッドは機能(Function)みたいな意味合いである
設計とはアプリケーションの可変性を保つために技巧を凝らすことであり、完璧を目指すための行為ではありません。 Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.834-835). Kindle 版.
変更がかんたんなようにコードを組成する
変更が簡単とは
- 変更は副作用をもたらさない
- 要件の変更が小さければ、コードの変更も相応して小さい
- 既存のコードはかんたんに再利用できる
- 最もかんたんな変更方法はコードの追加である。ただし追加するコードはそれ自体変更が容易なものとする
書くコードに必要な性質(TRUE)
- 見通しが良い(Transparent):変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響が明白である
- 合理的(Reasonable):どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい
- 利用性が高い(Usable):新しい環境、予期していなかった環境でも再利用できる
- 模範的(Exemplary):コードに変更を加える人が、上記の品質を自然と保つようなコードになっている
クラスが明確に定義された単一の責任を持つように徹底させる
2.2 単一の責任を持つクラスをつくる
なぜ単一責任が重要なのか
- 変更が簡単なアプリは、再利用が簡単なクラスから構成される。
- 着脱可能なユニット
- メソッドを分けようと思ったら、クラスごとに分離する方が良い
- 同じクラス内でメソッドを膨らませるのは良くない
クラスが単一責任かどうかを見極める
以下のような方法がある。
あたかもそれに知覚があるかのように仮定して問いただすこと
- クラス(主語)でメソッド(機能)が成り立っているか質問する
最小で有用なことは良いクラスである。
1文でクラスを説明してみること
以前の話を思い出せば、クラスはできる限り最小で有用なことをすべきでした。それはかんたんに説明できるものであるべきです。考えつく限り短い説明に「それと」が含まれていれば、おそらくクラスは2つ以上の責任を負っています。「または」が含まれるようであれば、クラスの責任は2つ以上あるだけでなく、互いにあまり関連もしない責任を負っていることがわかるでしょう。
上記のような方法を表す言葉として「凝縮度」
凝縮は、1個のメソッドにどれだけ詰まっているかという認識である。
責任駆動設計(RDD:ResponsibilityDrivenDesign)という概念に由来します。彼らによれば「クラスはその目的を果たすための(複数の)責任を持つ」のです。
理解としては以下のようなイメージです。
責任駆動設計 > 単一責任の原則
クラスがすることはすべて、そのクラスの目的に強く関連することを求めること
とは、以下の2つのどちらかに当てはまっていること
- あたかもそれに知覚があるかのように仮定して問いただすこと
- 1文でクラスを説明してみること
設計を決定するときを見極める
アプリケーションについての情報をより多く持つことが大事である。
アプリケーションの情報はどんどん更新されていくため、どんなに精通してても初期開発時が一番情報が少ない
情報が増えていく中で、新しい依存関係は優れた設計に必要な情報を与えてくれる
(コストを鑑みて)タイムリーな修正が大事
2.3 変更を歓迎するコードを書く
どんな変更が発生することが起きるか予測することができないのでコードの拡張性を保つコードを意識する。
データではなく、振る舞いに依存する
- DRYに書くこと
- データのアクセス方法を考慮する必要がある
あらゆる箇所を単一責任にする
メソッドから余計な責任を抽出する 役割が何であるか質問をし、また1文で責任を説明できるようにする(クラスと同じ)
- 設計の変更
- パフォーマンスの向上
設計が明確でないから小分けにしておくことは大切である
- 隠蔽されていた性質を明らかにする
- コメントをする必要がない
- 再利用を促進する
- 他のクラスへの移動が簡単
クラスの余計な責任を隔離する
- メソッドを単一責任にすれば、クラスのスコープはより明確になる
- 設計上の選択をある/ないの二択にしてしまうのは短絡的
二択化しないことで以下のようなことがわかる
- 必要な理由が見えてくる。
- その時点でやる理由が曖昧ならやる必要はないけど、覚えてはおけ
- 大事なクラスに集中する
2.5 まとめ
まとめは本文参照です。
変更可能でメンテナンス性の高いオブジェクト指向ソフトウェアへの道のりは、単一責任のクラスからはじまります。1つのことに専念するクラスは、その1つのことをアプリケーションのほかの部位から「隔離」します。この隔離によって、悪影響を及ぼすことのない変更と、重複のない再利用が可能となるのです。 Sandi Metz. オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 (Japanese Edition) (Kindle の位置No.1318-1321). Kindle 版.
参考書籍
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon