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

Tony Duong

Tony Duong

7月 15, 20261

他の言語:🇫🇷🇬🇧
#poodr#ruby#object-oriented-design#modules#roles#sandi-metz
POODR 第7章:モジュールでロールの振る舞いを共有する

概要

第6章では、同じもののバリエーションであるクラス間で振る舞いを共有するために古典的継承を使いました(RoadBike Bicycle である)。しかし、それ以外は無関係な複数のオブジェクトが同じロール(role)を演じる必要がある場合もあります。つまり、同じものであることなく、同じことをする必要があるのです。第7章では、そうしたロールの振る舞いをRubyのモジュール(mixin)で共有する方法を扱い、続いて、古典的なものであれモジュールベースのものであれ、あらゆる継承を信頼できるものにするルールを提示します。

ロールを理解する

問題によっては、無関係なオブジェクトが同じメッセージに応答する必要があります。その共有された責務が**ロール(role)**です。duck type(第5章)はインターフェースによって定義されるロールですが、本章では共有コードも伴うロールを扱います。

はじめに一言注意を。ロールを使うと依存が生まれます。そしてその依存は設計のリスクを高めます。ロールは強力ですが、反射的にではなく、意図的に使うべきです。

ロールを見つける

例:スケジューラは、対象(BicycleMechanicVehicle)が提案された期間中に利用可能かどうかを、予約間の「リードタイム(lead time)」を尊重しつつ知る必要があります。スケジュール可能なものはすべて schedulable? に答えなければならず、それらは同じリードタイムのロジックを共有します。しかし自転車、整備士、車両はそれ以外では無関係です。

これが Schedulable ロールです。インターフェースとその共有された振る舞いは、どんなクラスでもincludeできるモジュールに属します。

ロールを使うコードを書く — モジュール

Rubyでは、**モジュール(module)**は名前の付いたメソッドの集合を保持し、include によってどんなクラスにも混ぜ込むことができます。いったんincludeされると、モジュールのメソッドは、まるでそのクラスで定義されたかのように、そのクラスのインスタンスから利用できるようになります。オブジェクトがその振る舞いを獲得するのです。

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

  # includers may override; template-method style default
  def lead_days
    0
  end
end

class Bicycle
  include Schedulable

  def lead_days
    1
  end
end

Schedulableアルゴリズムschedulable?scheduled?)を定義し、妥当なデフォルト値を持つフックlead_days)を公開します。各includerは異なる部分だけを特殊化します。これはまさに第6章のtemplate methodパターンであり、今度はスーパークラスではなくモジュールを通して適用されています。

メソッド探索とアンチパターン

オブジェクトがメッセージを受け取ると、Rubyは対応するメソッドを特定の順序で探します。

  1. オブジェクト自身のクラス
  2. そのクラスがincludeしたモジュール(最後にincludeされたものが最初に探される)、
  3. スーパークラス
  4. スーパークラスがincludeしたモジュール、
  5. ObjectKernelBasicObject までチェーンをたどる。

includeされたモジュールはこの探索パスに挿入されるため、モジュールを混ぜ込むことは継承の一形態です。同じルールと同じリスクが適用されます。

次の2つのアンチパターンは、ロールや抽象化に手を伸ばすべきサインです。

  • self にどのメッセージを送るかを決めるために、typecategory のような名前の変数を使うオブジェクト → それらのクラスはおそらくロールを共有しています。is-a の部分には古典的継承を、behaves-as の部分にはモジュールを検討しましょう。
  • どのメッセージを送るかを決めるために受け手のクラスをチェックするメッセージ送信者 → duck typeが欠けています。受け手たちはロールを共有しており、そのロールのインターフェース(そしておそらく共有コード)はモジュールに属します。

継承可能なコードを書く

最後の節では、スーパークラス経由であれモジュール経由であれ、あらゆる継承に適用されるルールを示します。これらこそが、階層を土台として安全に築けるものにします。

アンチパターンを認識する

(上記のとおり)— それらはコードを共有するのが適切なときを教えてくれます。

抽象化にこだわる

抽象スーパークラスやモジュールのすべてのコードは、それを継承するあらゆるオブジェクトに適用されなければなりません。一部のincluderだけが必要とするコードを決してそこに置いてはいけません。サブクラスやincluderがメソッドをオーバーライドしてエラーを投げる(「これは実装していない」)なら、それは抽象化が誤っている証拠です。そのメソッドは共有コードに属していなかったのです。

契約を守る — Liskov Substitution Principle

サブクラスとincluderは、それらが特殊化する対象と**置換可能(substitutable)**でなければなりません。サブクラスは同じインターフェースを守り、呼び出し側が期待する形で振る舞うべきです。つまり同じ種類の入力を受け付け、同じ種類の出力を返すのです。これが Liskov Substitution Principle (LSP) であり、SOLIDの「L」です。これに違反すると、呼び出し側はどの具象型を扱っているのかを知らねばならなくなり、抽象化の存在意義がすべて失われます。

template methodパターンを使う

抽象的なもの(共有アルゴリズム)と具体的なもの(includerごとの特殊化)を分離します。共有コードがincluderの実装するメッセージを送るようにし、デフォルト値は共有コード側で用意します。

先手を打ってクラスを疎結合にする — includerで super を避ける

可能な限り、includer/サブクラスに super の送信を強いるコードを書くのは避けましょう。代わりに**フックメソッド(hook method)**を使い、特殊化がアルゴリズムを知らなくてもそこに差し込めるようにします。こうすれば、共有コードはいつの制御を保ち、includerは何をの制御を保てます。

浅い階層を作る

深く広い継承階層は理解しにくく、変更にコストがかかります。メソッド探索が遠くまで移動し、振る舞いがどこから来るのかを推論するのが難しくなるからです。浅く狭い階層を選びましょう。理解しやすく、拡張のリスクもはるかに小さくなります。

重要なポイント

  1. モジュールを使えば無関係なクラスがロールを共有できる — 同じものであることなく同じことをするオブジェクトのための共通の振る舞い。
  2. ロールを使うと依存が生じるので、デフォルトではなく意図的にモジュールを適用する。
  3. モジュールをincludeすることは継承の一形態 — モジュールはメソッド探索パスに加わるため、継承のすべてのルールがモジュールにも適用される。
  4. モジュールでtemplate methodパターンを適用する:アルゴリズムを定義し、妥当なデフォルトを持つフックを公開し、includerに特殊化させる。
  5. 抽象化にこだわる — 共有コードはあらゆるincluderに適用されなければならない。「未実装」を投げるオーバーライドは、抽象化が誤っていることを意味する。
  6. Liskov Substitution Principleを守る — includer/サブクラスは、それらが拡張する抽象化と置換可能でなければならない。
  7. super を強いることを避け、階層を浅く狭く保つことで、結合と認知的負荷を最小化する。

🌐 Claudeによる翻訳

Tony Duong

著者: Tony Duong

デジタル日記。思考、経験、そして人生についての考え。