POODR 第2章:単一責任を持つクラスを設計する

Tony Duong

Tony Duong

7月 15, 20262

他の言語:🇫🇷🇬🇧
#poodr#ruby#object-oriented-design#single-responsibility#sandi-metz
POODR 第2章:単一責任を持つクラスを設計する

概要

第2章は、最初にして最も基礎的な目標について述べている。それは、一つのことだけをするクラスを作ることだ。単一責任を持つクラスは、再利用しやすく変更しやすい。なぜなら、その内部にあるものはすべて同じ理由で変化するからだ。この章では、今や有名な Gear/Wheel の自転車の例を使って、責任をどのように見つけ、隔離し、整理するかを示す。

クラスに何を含めるかを決める

設計の初期段階で難しいのは、クラスをどう書くかではなく、何を入れるかである。Metz の助言はこうだ。完璧な設計を最初から得ることよりも、変更しやすいようにコードを組織することのほうが重要だ。目指すべきは TRUE なコードである。

  • Transparent(透明)— 変更の結果が明白であること。
  • Reasonable(合理的)— 変更のコストがその利益に見合っていること。
  • Usable(利用可能)— コードが新しく予想外の文脈で再利用できること。
  • Exemplary(模範的)— コードが、それを変更する人に良い習慣を引き継がせるよう促すこと。

そこへ到達する方法が、Single Responsibility Principle(SRP)を守ることだ。クラスは可能な限り小さく有用なことをするべきであり、変更する理由は一つだけであるべきだ。

なぜ単一責任が重要なのか

多くのことをしすぎるクラスは再利用が難しい。ほしい振る舞いだけを取り出そうとしても、それ以外のすべてを引きずってこなければならないからだ。あるメソッドだけを新しい文脈にコピーしたくなったとき、それはそのクラスが一つ以上のことをしているという臭いである。

密結合した責任もまた、クラスを脆くする。ある振る舞いのために意図した変更が、無関係な別の振る舞いを誤って壊してしまうことがある。

クラスが単一責任を持つかどうかを見分ける方法

二つの実用的な手法がある。

  1. 文章のように問いただす。 クラスにそのメソッドを尋ね、質問として言い換えてみる。「ギアさん、あなたの ratio は何ですか?」は意味を成す。「ギアさん、あなたの gear_inches は何ですか?」は微妙だ。「ギアさん、あなたの tire_size は何ですか?」は明らかに意味を成さない — タイヤのサイズはギアではなくホイールに属するものだ。
  2. 一文で説明する。 最も単純な説明に「かつ(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 クラスはほとんど自ずと書き上がる。責任がすでにきれいに隔離されているからだ。

重要なポイント

  1. クラスは単一責任を持つべきだ — 変更する理由は一つ — それがクラスを再利用可能にし、安全に修正できるものにする。
  2. SRP をテストする には、メソッドを質問として問いただし、クラスを一文で説明してみる。「かつ(and)」と「または(or)」に注意する。
  3. TRUE なコードを目指す:Transparent、Reasonable、Usable、Exemplary。
  4. インスタンス変数をアクセサメソッドの背後に隠す ことで、データの定義点を一箇所にする。
  5. 複雑なデータ構造を隠す(たとえば Struct を使って)ことで、参照がレイアウトに依存しないようにする。
  6. メソッドでも単一責任を徹底する — 小さく焦点の絞られたメソッドは、将来の抽出と再利用を安価にする。
  7. 現れつつある責任を早めに隔離する。完全な新しいクラスを作る準備が整う前であっても。

🌐 Claudeによる翻訳

Tony Duong

著者: Tony Duong

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