<輪読会> オブジェクト指向実践ガイド -第7章モジュールでロールの振る舞いを共有する-

輪読会メンバー

  • Izumi Haruya

github.com

  • Sekine Yutaro

github.com

第7章モジュールでロールの振る舞いを共有する

  • 継承で解決できない問題
    • 例えば、2つのサブクラスに共通する性質を組み合わせする際、単純な継承では解決できない
    • 上記の場合、継承の手法を使い「ロール(役割)」を共有する解決法がある

7.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 版.

契約を守る

オブジェクトが期待どおりに振る舞うときで、かつ、サブクラスがスーパークラスのインターフェースに一致するように「期待される」ときのみである

リスコフの置換原則(LSP)

テンプレートメソッドパターンを使う

テンプレートメソッドパターンを使うと、抽象を具象から分けることができる。 抽象コードでアルゴリズムを定義し、抽象を継承する具象では、テンプレート化されたメソッドをオーバーライドすることで特化を行う

前もって疎結合にする

  • 継承する側でsuperを呼び出すようなコードを書くのは避けるべき
  • 代わりにフックメッセージを使う方が良い

フックメソッドは、superを送る問題は解決しますが、残念ながら、階層構造の階層が隣接していなければ役に立たない

階層構造は浅くする

  • 階層構造を浅く保つべき理由
    • 探索パス上にオブジェクトが多いとメッセージが通過するオブジェクトも増えます。すると、振る舞いが追加される機会も格段に増えます。オブジェクトは自身より上に位置するオブジェクト「すべて」に依存するので、階層構造が深ければ、その分だけ大量の依存をはじめから持つことになる

7.3まとめ

  • (7.2 ※アンチパターン)の考えを用いて、継承かモジュールを使用することを検討する
  • 継承とモジュールの使い分けに関しては、リスコフの置換原則を用い、メソッド探索の仕組みを考慮して、どちらで実装するかを検討する
  • 継承、モジュールを実装する具体的な方法としてテンプレートメソッドパターンを使用すると好ましい
  • superを使用しないことやフックメッセージを使用することで疎結合にする
  • 階層構造を深くしないことで、不要な依存関係を取り除くことができる

参考書籍

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山泰基 | 工学 | Kindleストア | Amazon