「オブジェクト指向設計実践ガイド」要点
書籍
この記事について
個人のメモとして、下記の事項のみを抜粋して記載している。
- 自分が今後使いそうな内容
- 忘れそうな内容
- 初めて知って、感動した内容
1章 オブジェクト指向設計
設計とは
後の変更を容易にする行い。
コードの構成こそが設計。 設計(コードの構成)は芸術である。
オブジェクト指向設計とは
依存関係を管理すること。
- オブジェクト: 部品。
- メッセージ: オブジェクト間で受け渡されるもの。相互作用。
オブジェクト指向設計の道具
設計する際に設計者を助ける道具として、原則とパターンがある。
設計原則
SOLID
設計原則の1つ。
- Single Responsibility Principle:単一責任の原則
- Open/closed principle:オープン/クロースドの原則
- Liskov substitution principle:リスコフの置換原則
- Interface segregation principle:インターフェース分離の原則
- Dependency inversion principle:依存性逆転の原則
(感想:設計原則について、SOLIDの他にどんなものがあるかは下記記事が参考になった。)
デザイン(設計)パターン
いわゆるGoF
2章 単一責任のクラスを設計する
オブジェクト指向設計のシステムの基礎は(クラスでなく)メッセージである。
(メッセージこそが設計の核だが、クラスのほうが分かりやすいので、クラスから解説している。)
変更が容易なコードに求められる性質 (TRUE)
見通しが良い(Transparent)
変更するコードも、そのコードに依存する別の場所のコードにおいても、変更による影響が明白であること。(影響範囲がわかるか)
合理的(Reasonable)
どんな変更であっても、かかるコストは変更がもたらす利益にふさわしい。(低コストか)
利用性が高い(Usable)
新しい環境、予期していなかった環境でも再利用できる。(どこでも動くか)
模範的(Examplary)
後からコードに変更を加える人が、上記の品質を自然と保つようなコードである。(他人を導くことができるか)
なぜ単一責任が重要なのか
- 責任が互いに結合しすぎて、必要な振る舞いだけを取り出すこと(再利用)ができないため。
- 変更を加えるたびに、そのクラスに依存するクラスすべてを破壊する危険性があるため。
クラスが単一責任かどうかを見極める
- クラスメソッドを質問に言い換える。
- 「Gearさん、この自転車のギアの比率を教えてくれますか?」→OK
- 「Gearさん、この自転車のタイヤのサイズを教えてくれますか?」→NG
- クラスの責任を1文で言い表す
- 「自転車へのギアの影響を計算する」→OK
- 「自転車へのギアの影響と、タイヤの円周を計算する」→NG
(感想:とても重要。今後クラスを設計するときには必ず行う。)
変更を歓迎するコードを書く
実際に変更が起こるかorどんな変更がおこるかは分からないが、容易に変更を受け入れられるコードを書くことは、将来的に大きな見返りとなる。
以下は、そのテクニック。
データでなく、振る舞いに依存する
データへのアクセスは2つのどちらかの方法で行われるが、2.を使うようにする。
- インスタンス変数を直接参照する
- インスタンス変数をアクセサメソッドで包み隠す(ゲッタを経由させる)
インスタンス変数の隠蔽 (2.)
class Gear
# ゲッタを用意 → インスタンス変数の隠蔽
attr_reader :cog
def initialize(cog)
@cog = cog
end
end
attr_reader
やattr_accessor
を使わない(ゲッタを用意しない)場合、例えば@cogを修正する必要が生じたとき、@cogの出現箇所すべてを修正する必要がある。
それに対し、attr_reader
は裏で下記の実装をしてくれているため、1箇所だけの修正で変化に対応できる。
def cog
@cog # ここを修正すればいい
end
つまり、@cogというデータでなく、cogという振る舞い(アクセサメソッド)に依存するようにする。
あらゆる箇所を単一責任にする
メソッドから余計な責任を抽出する
(クラスと同様の理由で)メソッドも単一責任であるべき。
メソッドに対しても、その役割を1文で説明できるようにすること。
# 複数のタイヤの直径を取得
def diameters
wheels.collect { |wheel|
wheel.rim + (wheel.tire * 2) # 直径を計算
}
end
# 複数のタイヤの直径を取得
def diameters
wheels.collect { |wheel| diameter(wheel) }
end
# (1つの)タイヤの直径を計算
def diameter(wheel)
wheel.rim + (wheel.tire * 2)
end
3章 依存関係を管理する
適切に設計されたオブジェクトは単一の責任を持つ。そのため、目的のためにはオブジェクト同士の共同作業が必要であり、共同作業をするにオブジェクトは他のオブジェクトを知っていないといけない。「知っている」というのは依存であり、しっかり管理する必要がある。
依存関係とは
クラス間に一定の依存関係が生まれるのは避けられないが、依存は最低限にするべき。(コードの合理性が失われるため)
以下のコードには、依存関係がある。(Wheelの変更によって、Gearの変更が強制される状況。)
class Gear
def gear_inches
ratio * Wheel.new(rim, tire).diameter # ギア比率 * タイヤの直径
end
end
オブジェクトが次のことを知っているとき、オブジェクトには依存関係がある。
-
他のクラスの名前
Gearは、Wheelという名前のクラスが存在することを知っている。 -
self以外のどこかに送ろうとするメッセージの名前
Gearは、Wheelのインスタンスがdiameterに応答することを知っている。 -
メッセージが要求する引数
Gearは、Wheel.newにrimとtireが必要なことを知っている。 -
引数の順番
Gearは、Wheel.newの第1引数がrimで、第2引数がtireである必要があることを知っている。
オブジェクト間の結合
2つ以上のオブジェクトの結合が強固なとき、それらは1つのユニットであるように振る舞う。
つまり、1つだけを再利用する ということができない。
疎結合なコードを書く
依存オブジェクトの注入
class Gear
- attr_reader :chainring, :cog, :rim, :tire
+ attr_reader :chainring, :cog, :wheel
def initialize(chainring, cog, rim, tire)
@chainring = chainring # チェーンリングの歯数
@cog = cog # コグの歯数
- @rim = rim # リム(タイヤの内側の金属部分)の直径
- @tire = tire # タイヤの厚み
+ @wheel = wheel # wheelオブジェクト
end
def gear_inches
- ratio * Wheel.new(rim, tire).diameter
+ ratio * wheel.diameter # ギア比率 * タイヤの直径
end
end
- puts Gear.new(52, 11, 26, 1.5).gear_inches
+ puts Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches # Gearが依存しているWheelオブジェクトを注入
依存が削減され、今はwheelがdiameterメソッドに応答することだけ知っている(1つだけ依存を残している)状況に改善。
外部メッセージを隔離する
gear_inchesのwheel.diameter
は、Gearにおいては外部メッセージ。
def gear_inches
ratio * wheel.diameter # selfへのメッセージ+外部メッセージ (外部メッセージが含まれている)
end
今は簡素なコードだから良いが、gear_inchesが複雑になるほどこの外部メッセージによって変更が必要になる可能性(壊れる危険性)が高くなる。
そこで、gear_inches内の外部的な依存を取り除くため、専用のメソッド内にカプセル化する。
def gear_inches
ratio * diameter # selfへのメッセージのみ
end
def diameter
wheel.diameter # 外部メッセージ (カプセル化)
end
引数の順番への依存を除去する
引数が必要なメッセージを送るとき、引数を「正しい順番」で渡す必要がある場合、それは引数の順番に依存している。
# 第1引数はchainringで, 第2引数はcog, 第3引数はwheel であることを知っている必要がある
Gear.new(52, 11, Wheel.new(26, 1.5))
Gear.new(chainring: 52, cog: 11, wheel: Wheel.new(26, 1.5))
依存方向の管理
依存関係には常に方向がある。依存方向の決め方を知る。
依存方向の選択
見定め方・考え方
- そのクラスは他のクラスより要件が変わりやすいか。
(変わりにくいものの例:Rubyの基本的なクラス、フレームワークのコード) -
具象クラスは、抽象クラスより変わる可能性が高い。
(抽象化されたものへの依存は、具象的なものへの依存よりも常に安全。) - 多くから依存されているクラスを変更すると、広範囲に影響がでる。
4章 柔軟なインターフェースをつくる
オブジェクト指向のアプリケーションはクラスから成り立つが、メッセージによって定義される。
設計では、オブジェクト間で受け渡されるメッセージについても考慮しなければならない。
→オブジェクトが何を知っているか(責任)や、誰を知っているか(依存関係)だけでなく、オブジェクトが互いにどうやって会話するかの設計が必要。
→オブジェクト間の会話はオブジェクトのインターフェースを介して行われる。
インターフェースを定義する
レストランを例にして表現。
-
レストランの厨房 = プライベートなメソッド
プライベートなメッセージが数多く受け渡されている -
メニュー表 = インターフェース
パブリックなメッセージ -
お客さん = オブジェクト
どのように料理が作れられているか(処理が行われているか)は知らなくていい。
クラス内のパブリックなメソッドは安定した部分であり、プライベートなメソッドは変化し得る部分。
一番良い面(インターフェース)を表に出すコードを書く
インターフェースの明快さは、設計スキルを表す。
インターフェースは、下記のようにあるべき。
- パブリックかプライベートかが明らか。
- 「どのように」でなく「何を」になっている。
- 名前は変わり得ない。(考えられる限り)
- オプション引数としてハッシュをとる。
メソッドのアクセス制御
Rubyには下記3種のアクセス制御がある。
-
public
: 制限なし。クラスの外からでも呼び出すことができる。 -
private
: クラス内でのみ呼び出せる。クラスの外から呼び出せない。 -
protected
: クラス内+同じクラスのインスタンスなら呼び出せる。(特殊)
(下記サイトがわかりやすく解説していた)
これらの制御を使う用途は下記2つであり、全く異なるものである。
-
どのメソッドが安定していて、どのメソッドが不安定か を示すため。
private
が最も不安定。(publicが安定。) - アプリケーションのほかのところに、どれだけメソッドが見えるか を制御するため。
それでも使うのは、上記1.の通りそのメソッドの安定性を示すためである。
デメテルの法則
3つ目のオブジェクトにメッセージを送る際に、異なる型の2つ目のオブジェクトを介することを禁止する。
(直接の隣人にのみ話しかけよう、ドットは1つまでにしよう と表現される。)
customer.bicycle.wheel.tire
メッセージチェーン内のどこかで起きる関係のない変更によって、変更を与儀なくされるリスクが高まっている。
5章 ダックタイピングでコストを削減する
動的型付けオブジェクト指向プログラミング言語で使われる型付けのやり方のこと。
(由来:そのオブジェクトがアヒルのように鳴き、アヒルのように歩くならば、そのクラスが何であれ、それはアヒルである。)
ダックタイピングを理解する
ダックを見逃す
以下は、ダックタイピングする前のコード。
class Trip
attr_reader :bicycles, :customers, :vehicle
# 引数のmechanicは、どんなクラスのものでも良いことになっている
def prepare(mechanic)
# しかし、mechanicがprepare_bicyclesに応答するはずだと信じている
# → prepare_bicyclesに応答できるオブジェクトが渡される ことに依存している
mechanic.prepare_bicycles(bicycles)
end
end
class Mechanic
def prepare_bicycles(bicycles)
bicycles.each {|bicycle| prepare_bicycle(bicycle)}
end
def prepare_bicycle(bicycle)
# 何かの処理
end
end
問題を悪化させる
あえて問題を悪化させて考えるために、下記のように要件が変わったとする。
「旅行の準備には、整備士に加え、旅行のコーディネーターと運転手も必要になった」
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
# prepare_bicyclesに応答できないオブジェクトにも対応するためcase文で場合分け
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when TripCoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
end
}
end
end
# (その他のクラスは略)
ここでの問題点は、Tripが具象クラスとそのメソッドを知りすぎていること。
ダックを見つける
ダックを見つける思考を下記に示す。
- 依存を取り除く(ダックを見つける)鍵となるのは、「Tripクラスのprepareメソッドは単一の目的を達成するためにあるので、その引数も単一の目的を達成するために渡されてくる」と認識すること。
- prepareの目的は、旅行を準備すること。
- その引数も、旅行の準備に協力しようとやってくる(渡されてくる)。つまり、引数はすべて準備するもの(Preparer)だと考える
- TripはPreparer(整備士、コーディネーター、運転手)に何をしてほしいか。それは旅行に行くための準備(prepare_trip)。
- よって、Preparerは皆、prepare_tripに応答できればいい。
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each { |preparer| preparer.prepare_trip(self) }
end
end
class Mechanic
def prepare_trip(trip)
trip.bicycles.each { |bicycle| prepare_bicycle(bicycle) }
end
def prepare_bicycle(bicycle)
# 何かの処理
end
end
class TripCoordinator
def prepare_trip(trip)
buy_food(trip.customers)
end
# ...
end
class Driver
def prepare_trip(trip)
gas_up(trip.vehicle)
end
# ...
end
隠れたダックを見つける
既存のコード内にダックタイプが潜んでいることがある。
よく使われるコーディングパターンの中には、その存在を示唆するものがある。
1. クラスで分岐するcase文
前述のコードで示した内容。(オブジェクトのクラスによって分岐させていた。)
kind_of?
とis_a?
2. kind_of?
とis_a?
も、そのオブジェクトのクラスを確認するものなので、上記と同じ。
responds_to?
3. responds_to?
はオブジェクトにメソッドがあるか調べる。
そのオブジェクトのクラスを確認することと、そのオブジェクトがメッセージに応答するかどうか確認することは、この文脈上同じ。(そのオブジェクトが何を実行できるか知っている)
6章 継承によって振る舞いを獲得する
継承したクラスが応答できないメッセージ(メソッドが無い場合)は、親クラス(スーパークラス)に自動的に問い合わせる。
(自分で応答できる場合は当然、自分で応答する。)
例:Rubyのnil?メソッド
すべてのクラスの親クラスであるObjectクラスにはnil? -> false
と定義してある。
Nillクラスにはnil? -> true
と定義されてある。
そうすることで、Nillインスタンス.nil?はtrueに、その他のインスタンスはfalseを返すようになっている。
継承を使うべき箇所を識別する
以下、2種類の自転車(ロードバイク、マウンテンバイク)を例にする。
複数の型を埋め込む定義したクラス
class Bicycle
attr_reader :style, :size, :tape_color, :front_shock, :rear_shock
def initialize(args)
@style = args[:style] # 自転車の種類
@size = args[:size] # 自転車のサイズ
@tape_color = args[:tape_color] # ハンドルテープの色
@front_shock = args[:front_shock] # 前のサスペンション(マウンテンバイク特有)
@rear_shock = args[:rear_shock] # 後ろのサスペンション(マウンテンバイク特有)
end
# スペアとして用意するもの
def spares
if style == :road
# ロードバイクの場合
{
chain: '10-speed',
tire_size: '23',
tape_color: tape_color
}
else
# ロードバイク以外(マウンテンバイク)の場合
{
chain: '10-speed',
tire_size: '2.1',
rear_shock: rear_shock
}
end
end
end
継承を不適切に使う
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock] # 前のサスペンション(マウンテンバイク特有)
@rear_shock = args[:rear_shock] # 後ろのサスペンション(マウンテンバイク特有)
super(args) # 親クラスの同メソッド呼び出し
end
def spares
super.merge(rear_shock: rear_shock)
end
end
このようにクラスを定義してはいけない。
なぜなら、Bicycleが具象クラスのままで、Bicycleにはロードバイクがまだ含まれている。
つまり、MountainBike(具象クラス)がロードバイクの振る舞いを継承してしまっている。
→ Bicycle(親クラス)を抽象クラスにして、RoadBikeをBicycleから分離すべき。
抽象的な親クラスをつくる
Bicycleから分離し、RoadBikeクラスを新規で作成。
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size] # 自転車のサイズ
@chain = args[:chain]
@tire_size = args[:tire_size]
end
def spares
{
chain: chain,
tire_size: tire_size,
}
end
end
class RoasBike < Bicycle
attr_reader :tape_color
def initialize(args)
@tape_color = args[:tape_color] # ハンドルテープの色
super(args) # 親クラスの同メソッド呼び出し
end
def spares
# {
# chain: '10-speed',
# tire_size: '23',
# tape_color: tape_color
# }
super.merge(tape_color: tape_color)
end
end
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock] # 前のサスペンション(マウンテンバイク特有)
@rear_shock = args[:rear_shock] # 後ろのサスペンション(マウンテンバイク特有)
super(args) # 親クラスの同メソッド呼び出し
end
def spares
# {
# chain: '10-speed',
# tire_size: '2.1',
# rear_shock: rear_shock
# }
super.merge(rear_shock: rear_shock)
end
end
テンプレートメソッドパターンを使う
テンプレートメソッドパターン
親クラスで抽象的に決めて、子クラスで詳細を埋める。
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size]
# 初期値として渡されない限りは、自転車共通の値を採用する
- @chain = args[:chain]
+ @chain = args[:chain] || default_chain
# 初期値として渡されない限りは、各自転車で指定している特有の値を採用する
- @tire_size = args[:tire_size]
+ @tire_size = args[:tire_size] || default_tire_size
end
+ def default_chain
+ '10-speed' # どんな自転車でも共通の初期値
+ end
def spares
{
chain: chain,
tire_size: tire_size
}
end
end
class RoasBike < Bicycle
attr_reader :tape_color
def initialize(args)
@tape_color = args[:tape_color]
super(args)
end
+ def default_tire_size
+ '23' # ロードバイク特有の初期値
+ end
def spares
super.merge(tape_color: tape_color)
end
end
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
super(args)
end
+ def default_tire_size
+ '2.1' # マウンテンバイク特有の初期値
+ end
def spares
super.merge(rear_shock: rear_shock)
end
end
すべてのテンプレートメソッドパターンを実装する
上の実装で各パラメータが存在すべき場所に存在できるようになったが、新たな具象クラスを作るときに問題を引き起こす可能性がある。
具象クラスにはdefault_tire_size
の実装が必須だが、そのことを知らずに新たな***Bike
クラスを作り、特にtire_size
を指定せず初期化するとエラーが起こる。
→ 一見しただけでは把握できない要件(default_tire_size
が必須)を、親クラスが子クラスに課している。
class Bicycle
attr_reader :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_tire_size
+ # なぜエラーが起きたのか明確にする
+ raise NotImplementedError, "#{self.class}クラスはこのメソッドに応答できない:"
+ end
# 略
end
# 略
フックメッセージを使って子クラスを疎結合にする
上のコードにおいても改善点があり、それは子クラスのinitialize
。
親クラスがhashを返すこと(アルゴリズム)を知っていることがまずい。
(例えば、新しい子クラスRecumbentBike
を作り、initialize
でsuper
を書き忘れると、指定した値が初期値として設定されずnilとなる。)
子クラスから親クラスにsuperを送るように(親クラスが)求めるのでなく、親クラスが子クラスにメッセージを送るようにすることで、それを解消する。
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args = {})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
+ post_initialize(args)
# -> 子クラスにpost_initializeメッセージを送る(無ければこのクラスのpost_initializeに)
end
+ def post_initialize(args)
+ nil
+ end
# 略
end
class RoasBike < Bicycle
attr_reader :tape_color
- def initialize(args)
+ def post_initialize(args)
@tape_color = args[:tape_color]
- super(args) # -> 親クラスにメッセージを送らなくて良くなった
end
# 略
end
7章 モジュールでロールの振る舞いを共有する
モジュール
プログラム上での役割や振る舞いをまとめることができる。
クラスと同じように定数やメソッドをまとめたり、クラスに組み込んで多重継承を実現したり、クラスなどをまとめることで名前空間を提供するなど。
ロールを理解する
例題として、旅行のスケジュールを知ることができるように、機能を追加する。
- 予定が空いているか確認が必要なのは、自転車・整備士・自動車。
- 前の旅行からのメンテ(休息)期間は、自転車1日・整備士4日・自動車3日。
モジュールを使って表現する。
class Schedule
def scheduled?(schedulable, start_date, end_date)
false # ひとまず、予定が空いていないことにしておく
end
end
module Schedulable
attr_writer :schedule
def schedule
@schedule ||= ::Schedule.new
end
def schedulable?(start_date, end_date)
!scheduled?(start_date - lead_days, end_date)
end
def scheduled?(start_date, end_date)
schedule.scheduled?(self, start_date, end_date)
end
def lead_days
0 # 必要に応じてincludeする側で書き換える
end
end
class Bicycle
include Schedulable
def lead_days
1 # 自転車のメンテ期間は1日
end
# 略
end
class Mechanic
include Schedulable
def lead_days
4 # 整備士の休息期間は4日
end
# 略
end
メソッド探索の仕組み
bike.spares
を実行したとき、sparesメソッドがあるかどうかは、下記のように探索される。
- MountainBikeクラスにsparesメソッドがあるか確認。
- Bicycleクラス(親)にsparesメソッドがあるか確認。
- Objectクラス(親の親)にsparesメソッドがあるか確認。
モジュールをinclueしている場合
- MountainBikeクラスにsparesメソッドがあるか確認。
- Bicycleクラス(親)にsparesメソッドがあるか確認。
- Schedulableモジュールにsparesメソッドがあるか確認。
- Objectクラス(親の親)にsparesメソッドがあるか確認。
そのため、Schedulableモジュールに定義したメソッドを、意図せずBicycleクラスがオーバーライドする可能性がある。
(複数includeしている場合は、最後にincludeしたものが先に探索される。)
継承可能なコードを書く
アンチパターン
- オブジェクトがtypeやcategoryという変数名を使い、どんなメッセージをselfに送るか決めているパターン。
→ 共通のコードを抽象親クラスにおき、子クラスで異なる型をつくる。 - メッセージを受け取るオブジェクトのクラスを確認してから、どのメッセージを送るかをオブジェクトが決めているパターン。
→ ダックタイプを見落としている。ダックタイプやモジュールを使い、ロールを担わせる。
抽象に固執する
抽象親クラス内のコードを使わない子クラスがあってはいけない。
すべての子クラスで使わないけど一部の子クラスで使うようなコードは、親クラスに置くべきでない。
リスコフの置換原則(LSP)
(SOLIDのL)
派生型は、上位型と置換可能でなければならない。
テンプレートメソッドパターンを使う
継承可能なコードを書くための最も基本的なコーディング手法は、テンプレートメソッドパターンを使うこと。
テンプレートメソッドパターンを使うと、抽象を具象から分けることができる。
前もって疎結合にする
継承する側でsuper
を呼び出すのは避ける。変わりにフックメッセージを使う。
階層構造は浅くする
クラスの階層構造は浅くする。(広さ、狭さ でなく。)
複雑になり理解が困難になるため、深くしてはいけない。
8章 コンポジションでオブジェクトを組み合わせる
コンポジションとは、組み合わされた全体が単なる部品の集合以上となるように、「部品」を「全体」へと組み合わせる行為。
以下、コンポジションのテクニック。
自転車をパーツからcomposeする
6章で作ったBicycleクラスは、継承を使った抽象親クラス。これをコンポジションを使うように変更する。
Bicycleクラスはspares
に応答する必要があるが、パーツから構成される自転車は、Partsクラスを持ち、それがspares
に応答できればいい。
部品群を表すPartsクラスを作成し、自転車の種類(ロードバイクなど)もPartsの一部として考える。
class Bicycle
attr_reader :size, :parts
def initialize(args = {})
@size = args[:size] # 自転車のサイズ
@parts = args[:parts] # 自転車の部品
end
def spares
parts.spares # 応答をpartsに委譲
end
end
class Parts
attr_reader :chain, :tire_size
def initialize(_atgs = {})
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
post_initialize(args)
end
def spares
{
chain: chain,
tire_size: tire_size
}.merge(local_spares) # 自転車ごとに特有のスペアを足す
end
# 子クラスでオーバーライドするメソッド群 -----
def default_tire_size
raise NotImplementedError
end
def post_initialize(_args)
nil
end
def local_spares
{}
end
# -----------------------------------
def default_chain
'10-speed' # どんな自転車でも共通の初期値
end
end
class RoasBikeParts < Parts
attr_reader :tape_color
def post_initialize(args)
@tape_color = args[:tape_color] # ハンドルテープの色
end
def local_spares
{ tape_color: tape_color }
end
def default_tire_size
'23' # ロードバイク特有の初期値
end
end
class MountainBikeParts < Parts
attr_reader :front_shock, :rear_shock
def post_initialize(args)
@front_shock = args[:front_shock] # 前のサスペンション(マウンテンバイク特有)
@rear_shock = args[:rear_shock] # 後ろのサスペンション(マウンテンバイク特有)
end
def local_spares
{ rear_shock: rear_shock } # マウンテンバイク独自で必要なスペアはrear_shockのみ
end
def default_tire_size
'2.1' # マウンテンバイク特有の初期値
end
end
Partsオブジェクトをcomposeする
Parts
クラスはPartの集まり(集合)であるため、Part
クラスを作る。
→ PartでPartsをcomposeする。
class Bicycle
attr_reader :size, :parts
def initialize(args = {})
@size = args[:size] # 自転車のサイズ
@parts = args[:parts] # 自転車の部品
end
def spares
parts.spares
end
end
# Partの集合
class Parts
attr_reader :parts
def initialize(parts)
@parts = parts
end
def spares
parts.select { |part| part.need_spare }
end
end
class Part
attr_reader :name, :description, :need_spare
def initialize(args)
@name = args[:name]
@description = args[:description] # 部品の詳細情報
@need_spare = args.fetch(:need_spare, true) # スペアを用意する必要があるか
end
end
コンポジションと継承の選択
一般的なルールとして、コンポジションで解決できるものであれば、コンポジションを使うべき。
コンポジションが持つ依存は、継承よりも少ないため。
継承
メリット
- 継承階層の頂点に近いところで定義されたメソッドの影響は広範囲に及ぶ。そのため、振る舞いの大きな変更を、コードの小さな変更で達成できる。
- 継承を使ったコードは「Open-Closed」となる。拡張には開いていて、修正には閉じている。
- 継承が正しく適用できれば、TRUE のR,U,Eにおいて優れる。
変更が容易なコードに求められる性質 (TRUE)
見通しが良い(Transparent)
合理的(Reasonable)
利用性が高い(Usable)
模範的(Examplary)
デメリット
- 継承が適さない問題に使用すると、簡単に振る舞いを追加できなくなる。
- 自分の書いたコードが、他のプログラマによって予期していなかった目的のために使われるかもしれなくなる。
- 継承が正しく適用できなければ、TRUE のR,U,Eにおいて副作用が起こり得る。
- 合理的(Reasonable)
間違ってモデル化された階層構造の頂点付近の変更にかかる、莫大なコストがかかる。(変更が困難) - 利用性が高い(Usable)
サブクラスが複数の型を混合したものの表現だったとき、振る舞いの追加が困難になる。 - 模範的(Examplary)
間違ってモデル化された階層構造を使って、(リファクタリングでなく)既存のコードを複製したり、クラス名への依存が追加されたりすることで、問題が悪化する。
コンポジション
メリット
- 責任が単純明快であり、明確に定義されたインターフェースを介してアクセスできる小さなオブジェクトが、自然と作られる傾向がある。
- 小さなオブジェクトなため見通しが良い。コードを簡単に理解でき、変更が起きた場合に何が起こるかが明確。上の階層構造の変更による副作用に悩まされることが基本的にない。
- composeされたオブジェクトは、自身のパーツとインターフェースを介して関わるため、新しい種類の部品の追加は、その部品に定義したインターフェースを持たせればいい。(あとは差し込むだけ)
デメリット
- 個々の部品は見通しが良くとも、組み合わされた全体の動作は理解しにくくなる。
- composeされたオブジェクトは、明示的にどのメッセージをだれに委譲するか、必ず知っておかなければならない。
- ほぼ同一のパーツを構成する問題に対してはそこまで助けにならない。
関係の選択
9章 費用対効果の高いテストを設計する
変更可能なコードを書くために必要なのは次の3つのスキル。
- オブジェクト指向設計の理解
- リファクタリングに長けていること
- 価値の高いテストを書く能力
意図を持ったテスト
テストにコストがかかるという問題への解決方法は、テストをやめることではなく、上手くなること。
→ テストから優れた価値を得るには、(テストの)意図の明確さが求められる。
何を、いつ、どのようにテストするかを知らなければならない。
テストの意図
バグを見つける
欠陥やバグを開発プロセスの初期段階で見つけることは、大きな利益である。(早ければ早いほど)
仕様書となる
信頼できる設計の仕様書となる。
設計の決定を遅らせる
設計の決定を安全に遅らせることができる。テストがインターフェースに依存している場合、その根底にあるコードは、奔放にリファクタリングできる。
設計の欠陥を明らかにする
テストのセットアップに苦痛が伴うのであれば、コードはコンテキストを要求しすぎている。
設計がまずければ、テストも難しい。(しかし、その逆は必ずしも真ではない。テストにコストがかかるからといって、必ずしも設計がまずい訳ではない。)
何をテストするかを知る
テストからより良い価値を得るための1つの単純な方法は、より少ないテストを書くこと。
貪欲すぎるテストは、対象のクラスをリファクタリングするたびに毎回壊れるせいで、コストを高めてしまう。
パブリックインターフェースに定義されるメッセージを対象としたテストを書くべき。
テストは、オブジェクトの境界に入ってくる(受信)か、出ていく(送信)メッセージに集中すべき。
- 受信メッセージ : その戻り値の状態がテストされるべき。
- 送信コマンドメッセージ : 送られたことがテストされるべき。
- 送信クエリメッセージ : テストするべきでない。
いつテストするかを知る
初級の設計者はテストファーストでコードを書くことが最も有益。
Discussion