第7章 モジュールでロールの振る舞いを共有する
はじめに
前回の記事では、6章をベースに「継承によって振る舞いを獲得する」について書きました。
今回はそれに続く第7章 モジュールでロールの振る舞いを共有するの学びをまとめていきます。
6章の「クラス継承」が縦の親子関係(is-a 関係)だったのに対し、7章では「モジュール(Mix-in)」を使い、クラスの垣根を越えて共通の「役割(behaves-like 関係)」をオブジェクトに持たせる手法を学んでいきます。
1. ロール(役割)を理解する
クラスという枠組みにとらわれず、オブジェクトが果たす「役割(ロール)」に着目します。
6章の継承が「マウンテンバイクは自転車である(is-a)」だったのに対し、7章は「自転車も、整備士も、トラックも、みんなスケジュール可能である(behaves-like Schedulable)」という、クラスをまたいだ共通の役割を扱います。
Rubyは多重継承をサポートしていませんが、モジュールを使うことで、無関係なクラス同士に同じ振る舞いを安全に共有させることができます。
2. アンチパターン:Scheduleクラスにリードタイムを持たせる
休息期間(リードタイム)を設定したいとします。
- 自転車:最低1日
- 自動車:最低3日
- 整備士:最低4日
最初に思いつくのは Schedule クラスにリードタイムを持たせる方法ですが、これは Schedule が各クラスの詳細を知りすぎてしまうアンチパターンです。これまで学んできたダックタイピングの出番です。
3. 具体的なコードから抽象を抽出する
まずは具体的に書いてみます。Bicycle と Mechanic にそれぞれ scheduled? を実装すると、コードが重複してしまいます。
class Bicycle
def scheduled?(start_date, end_date)
actual_start = start_date - lead_days
# ...複雑なカレンダーの計算処理...
end
def lead_days; 1; end
end
class Mechanic
def scheduled?(start_date, end_date)
actual_start = start_date - lead_days
# ...Bicycleとまったく同じ計算処理...
end
def lead_days; 4; end
end
scheduled? の処理がまったく同じなのに、クラスごとに重複して書かれています。これを解決するのが module です。
4. モジュールで抽象を共有する
共通のロジックを Schedulable モジュールに引き上げます。
module Schedulable
# 共通の計算処理はモジュールが引き受ける
def scheduled?(start_date, end_date)
actual_start = start_date - lead_days
# ...複雑な計算処理...
end
# サブクラスでのオーバーライドを強制する
def lead_days
raise NotImplementedError,
"This #{self.class} cannot respond to: #{__method__}"
end
end
class Bicycle
include Schedulable
def lead_days; 1; end
end
class Mechanic
include Schedulable
def lead_days; 4; end
end
各クラスは include Schedulable するだけで scheduled? を使えるようになり、自分の専門である lead_days だけを定義すればよくなりました。
5. メソッド探索の仕組みを理解する
モジュールを正しく使うためには、Rubyがどのようにメソッドを探しに行くか(継承チェーン)を知っておく必要があります。
クラス A にモジュール M を include すると、メソッドの探索順は以下のようになります。
クラスA → モジュールM → クラスAのスーパークラス
つまり、モジュールはクラスとスーパークラスの間に挟み込まれる形になります。複数のモジュールを読み込んだ場合は、最後にインクルードしたモジュールが最初に探索されます。
:: による名前空間の明示
モジュール内部に同名のクラスが存在する場合、:: を使ってどちらを参照するか明示できます。
class Schedule
def initialize
puts "本物のスケジュール帳です"
end
end
module Schedulable
class Schedule
def initialize
puts "モジュール内部のScheduleです"
end
end
def info
Schedule.new # => 「モジュール内部のScheduleです」
::Schedule.new # => 「本物のスケジュール帳です」
end
end
:: をつけることで、トップレベルのクラスを明示的に参照できます。
6. 継承可能なコードを書くためのガイドライン
モジュールや継承は強力ですが、使い方を誤るとコードを複雑にする危険もあります。
継承やモジュールを使うなら、このルールを守りましょう。
❌ アンチパターン:型による条件分岐
# type や category で case 分岐している → 継承・モジュールで分割すべきサイン
case type
when :bicycle then ...
when :mechanic then ...
end
❌ アンチパターン:相手のクラスを確認してからメッセージを変える
「このオブジェクトのクラスが X ならこのメソッドを呼ぶ」という処理は、ダックタイピングの原則を無視しています。
❌ アンチパターン:サブクラスで親のメソッドを「使えません」と上書きする
class MountainBike < Bicycle
def tape_color
raise NotImplementedError # 「自分はこの機能は使いません」← LSP違反!
end
end
親クラスの機能を継承しておきながら例外を投げるのは、リスコフの置換原則に違反しています。サブクラスは、スーパークラスと完全に置き換え可能でなければなりません。
✅ 階層構造は浅く保つ
階層が深くなると、底辺のサブクラスは上にあるすべてのスーパークラスに依存することになります。どこか1箇所が変更されただけで壊れるリスクが高まるため、階層はできるだけ浅く保ちます。
まとめ
| クラス継承(6章) | モジュール(7章) | |
|---|---|---|
| 関係 | is-a(〜である) | behaves-like(〜のように振る舞う) |
| 用途 | 本質的な性質を引き継ぐとき | クラスをまたいで役割を共有するとき |
| Rubyの仕組み | スーパークラスへの委譲 | インクルードしたモジュールへの委譲 |
この章全体を通して語られているのは、振る舞いを共有するためのルールと契約です。継承であれモジュールのインクルードであれ、「メソッド探索パスにコードを挿入する」という点では同じです。
その契約を安全に守り、変化に強いコードを書くための具体的な手段が「テンプレートメソッドパターン」「フックメッセージ」「浅い階層構造」です。
「オブジェクトは自身が主張するとおりに振る舞うべき」という原則こそが、オブジェクト指向設計の本質だと改めて感じました
普段の運用保守でコードを読んでいても、module がどういう意図で使われているのか、継承との違いは何なのか、ずっとモヤモヤしていました。この章を通じて「is-a(継承)」と「behaves-like(モジュール)」という使い分けの軸を得られたことで、既存コードを読む解像度が上がった気がします。設計する機会があれば、今回学んだパターンを意識して実践していきたいです!