「オブジェクト指向設計実践ガイド」の内容が大変ためになったため、すぐに振り返れるようにチェックシートを作りました。
単一責任クラス
- そのクラスの振る舞いとして正しいか。複数の役割を持っていないか。
- インスタンス変数を直接参照していないか。必ずアクセサでアクセスすること。
- 1メソッドが複数の責任をもっていないか。
複数の責任を持っている例
def diameters
ratio * (rim + (tire * 2)) # (rim + (tire * 2)) は別のメソッドに分ける
end
- 配列の何番目かを知っていないか。ClassやStructを使うこと。
- 変更が副作用をもたらさないか
- 要件の変更が小さければコードの変更も相応して小さいこと
- 再利用可能であるか
依存関係
- 他のクラスを知りすぎていないか。依存オブジェクトの注入に変えるべき。どうしても注入できない場合はインスタンス変数の作成をメソッドで包み隠す。
他者を知りすぎている例
class Gear
# (1)Wheelというクラス名を知っている
# (2)Weelインスタンスがdiameterという振る舞いができることを知っている
# (3)Wheel.newの引数がrim, tireであることを知っている
# (4)Whhel.newの引数の順番を知っている
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
end
注入できない場合は包み隠す
def wheel
@wheel ||= Wheel.new(rim, tire)
end
- 外部ライブラリの初期化が引数の順番に依存していないか。ラップしてライブラリ変更に対して自身を守るべき。
外部ライブラリの初期化をラップする例
def rapper(args)
# ThirdPartyに変更があっても影響を受けるのはここだけ
ThirdParty.new(args[:one], args[:two], args[:three])
end
rapper({one: 1, two: 2, three: 3})
- 依存関係が発生する場合は、将来変更されない方に依存すべき
- 注入により
具象クラス
から、◯◯メソッドを持つクラス
に依存することになる。Rubyにはインタフェースがないが脳内で考えてみるべき。
インタフェース
- オブジェクトが何を知っているか(
オブジェクトの責任
)、オブジェクトが誰を知っているか(オブジェクトの依存関係
)、どのようなメッセージをするか(インタフェース
) - 設計がうまくいかないときは
メッセージ
に集中すると見過ごしていたオブジェクトが明らかになる(例: Finderクラス) -
クラスに基づく設計
からメッセージに基づく設計
に思想を切り替えること - メッセージとは、
「受け手を信頼し送り手が望むことを頼む」
ものであり、「受け手がどのように振る舞うか伝える」
ものではない
メッセージの段階
(1)あなたがどのようにするか知っている # bad
(2)あなたが何をするか知っている # not good
(3)何をどのようにするか知らないが、私が望むことに対してあなたの担当をやってくれると信じている # good
例:
(1)旅行が整備士に対し自転車を掃除してと頼む
(2)旅行が整備士に対し自転車を準備してと頼む
(3)旅行が整備士に対し「旅行の準備がしたい」と頼み、整備士が旅行から自転車を取得し準備をしてくれる
-
パブリックメソッド
は、(1)そのクラスの主要な責任
(2)気まぐれで更新されない
(3)テストで振る舞いが定義されている
-
プライベートメソッド
は、(1)実装の詳細
(2)変更はあり得る
(3)テストは必ずしも必須ではない
ダックタイピング
- ダックタイピングに変えるべきアンチパターン
- オブジェクトのクラスを確認している(
case
kind_of?
is_a?
responds_to?
) -
type
やcategory
といった変数で処理を分けている
- オブジェクトのクラスを確認している(
- ダックタイピングには、継承やモジュールの中で
抽象的な共通処理
と具象化させる為のテンプレートメソッド
を用意するのがよく使う手段
継承
- 継承とは、
オブジェクトを階層構造に構成するコスト
を払う代わりにメッセージ委譲は無料で手に入る
というもの。このコストは変更時の膨大なコストに繋がる為、継承の方が良いとはっきり言える場合以外はコンポジション
を使うこと。 - 継承する場合は、「まずはメソッドをすべて下げていくつかを引き上げる」 が良い手順
- サブクラスによって変更したい部分はメソッドで包み隠しオーバーライドできるようにする(
テンプレートメソッドパターン
) -
テンプレートメソッドパターン
を使う場合は親にもNotImplementedError
を発生させるメソッドを用意すること - superは避け
フックメソッド
を使うようにすべき - スーパークラスのメソッドは全てのサブクラスで使われなければならない。そうでないクラスはサブクラスから外れるべき。
モジュール
- ロールが存在することを認識し、その
ダックタイプ
のインタフェース
としてモジュール
を定義する - 大抵の場合モジュールは
抽象コードでアルゴリズム
を定義し、テンプレートメソッドで具象化
する - ただし、共通で使う振る舞いがあれば、モジュールに具体的な処理を記述してもよい
-
継承 「 である(is_a) 」
、モジュール 「 のように振る舞う(behaves-like-a) 」
の違いは重要
コンポジション(小さなパーツの組み合わせ)
- 継承のコストとメリットを逆転させたもの。
-
明示的なメッセージ委譲というコスト
の代わりに独立して存在できる
というメリット -
継承
よりコンポジション
設計におけるまとめ
-
コンポジション
、クラスの継承
、モジュールを使った振る舞いの共有
という3つのテクニックがある。 - それぞれに
コスト
と利点
がある -
経験
を積み重ねることで選択の誤りがなくなっていく。失敗
から学びリファクタすることを恐れない
最後に、
「経験
を積み重ねることで選択の誤りがなくなっていく。失敗
から学びリファクタすることを恐れない」 ということが本当に大事だと感じました。学ぶ為にもリファクタをし続けていきたいと思います。