POODR 第2章:単一責任を持つクラスを設計する
Tony Duong
7月 15, 2026 ・ 2 分
概要
第2章は、最初にして最も基礎的な目標について述べている。それは、一つのことだけをするクラスを作ることだ。単一責任を持つクラスは、再利用しやすく変更しやすい。なぜなら、その内部にあるものはすべて同じ理由で変化するからだ。この章では、今や有名な Gear/Wheel の自転車の例を使って、責任をどのように見つけ、隔離し、整理するかを示す。
クラスに何を含めるかを決める
設計の初期段階で難しいのは、クラスをどう書くかではなく、何を入れるかである。Metz の助言はこうだ。完璧な設計を最初から得ることよりも、変更しやすいようにコードを組織することのほうが重要だ。目指すべきは TRUE なコードである。
- Transparent(透明)— 変更の結果が明白であること。
- Reasonable(合理的)— 変更のコストがその利益に見合っていること。
- Usable(利用可能)— コードが新しく予想外の文脈で再利用できること。
- Exemplary(模範的)— コードが、それを変更する人に良い習慣を引き継がせるよう促すこと。
そこへ到達する方法が、Single Responsibility Principle(SRP)を守ることだ。クラスは可能な限り小さく有用なことをするべきであり、変更する理由は一つだけであるべきだ。
なぜ単一責任が重要なのか
多くのことをしすぎるクラスは再利用が難しい。ほしい振る舞いだけを取り出そうとしても、それ以外のすべてを引きずってこなければならないからだ。あるメソッドだけを新しい文脈にコピーしたくなったとき、それはそのクラスが一つ以上のことをしているという臭いである。
密結合した責任もまた、クラスを脆くする。ある振る舞いのために意図した変更が、無関係な別の振る舞いを誤って壊してしまうことがある。
クラスが単一責任を持つかどうかを見分ける方法
二つの実用的な手法がある。
- 文章のように問いただす。 クラスにそのメソッドを尋ね、質問として言い換えてみる。「ギアさん、あなたの
ratioは何ですか?」は意味を成す。「ギアさん、あなたのgear_inchesは何ですか?」は微妙だ。「ギアさん、あなたのtire_sizeは何ですか?」は明らかに意味を成さない — タイヤのサイズはギアではなくホイールに属するものだ。 - 一文で説明する。 最も単純な説明に「かつ(and)」という言葉が使われるなら、そのクラスはおそらく一つ以上の責任を持っている。「または(or)」が使われるなら、その責任同士は関連すらしていない。良いクラスの説明は、単一で首尾一貫した目的を含んでいる。
変更を受け入れるコードを書く
最終的な設計がわかる前でさえ、柔軟なままでいられるコードを書くことはできる。
データではなく振る舞いに依存する
インスタンス変数を隠す。 メソッドの内部で @variables を直接参照してはならない — それらをアクセサメソッドで包み、データが定義される場所を一箇所にする。
class Gear
attr_reader :chainring, :cog # creates wrapper methods
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
chainring / cog.to_f # use the method, not @chainring
end
end
こうすれば、もし cog の意味がいつか変わる必要が生じても、あらゆる参照を探し回るのではなく、一つのメソッドを変更するだけで済む。
データ構造を隠す。 クラスが複雑な構造(たとえばホイールを表す2要素配列の配列)を受け取る場合、wheel[0] や wheel[1] のようなインデックス参照をあちこちに散らばらせてはならない。すべての参照は構造の正確なレイアウトに依存してしまう。代わりに、それを Struct で包み、各要素に名前を与える。
class ObscuringReferences
attr_reader :data
def initialize(data)
@data = data
end
def diameters
data.collect { |cell| cell[0] + (cell[1] * 2) } # what is [0]? [1]?
end
end
class RevealingReferences
attr_reader :wheels
def initialize(data)
@wheels = wheelify(data)
end
def diameters
wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) } # clear!
end
Wheel = Struct.new(:rim, :tire)
def wheelify(data)
data.collect { |cell| Wheel.new(cell[0], cell[1]) }
end
end
これで構造はちょうど一箇所にだけ存在するようになる。
どこでも単一責任を徹底する
同じ規律はクラスより下のレベルにも当てはまる。
- メソッドから余分な責任を抽出する。 メソッドも、クラスと同じく、一つのことをするべきだ。小さく単一目的のメソッドは、クラスの隠れた性質を明らかにし、コメントを不要にし、再利用や移動を容易にする。
- 余分な責任をクラスに隔離する。 メソッドや埋め込まれた構造が新しい概念(
Gearの中に住むWheelのような)を示唆するとき、それを抽出する — 最初は小さなStructの中へでもよい。すぐに完全な新しいクラスへ踏み切る必要はない。ただそのアイデアを隔離し、時が来たら移動できるようにしておけばよい。
その見返りとして、いずれ現れる Wheel クラスはほとんど自ずと書き上がる。責任がすでにきれいに隔離されているからだ。
重要なポイント
- クラスは単一責任を持つべきだ — 変更する理由は一つ — それがクラスを再利用可能にし、安全に修正できるものにする。
- SRP をテストする には、メソッドを質問として問いただし、クラスを一文で説明してみる。「かつ(and)」と「または(or)」に注意する。
- TRUE なコードを目指す:Transparent、Reasonable、Usable、Exemplary。
- インスタンス変数をアクセサメソッドの背後に隠す ことで、データの定義点を一箇所にする。
- 複雑なデータ構造を隠す(たとえば
Structを使って)ことで、参照がレイアウトに依存しないようにする。 - メソッドでも単一責任を徹底する — 小さく焦点の絞られたメソッドは、将来の抽出と再利用を安価にする。
- 現れつつある責任を早めに隔離する。完全な新しいクラスを作る準備が整う前であっても。
🌐 Claudeによる翻訳