🧐

オブジェクト指向設計実践ガイドを読む No.2【単一責任のクラスを設計する】

2021/01/17に公開

こんにちは。 sasumasa です。

今回はオブジェクト指向設計実践ガイドの第 2 章の「単一責任のクラスを設計する」について、僕の経験を踏まえながらまとめていきたいと思います。

なお、本記事は設計の目的や実践についてまとめた「オブジェクト指向設計実践ガイドを読む No.1」の続きとなっています。

https://zenn.dev/sasumasa/articles/810054ed9690ce

オブジェクト指向設計のシステムの基礎はメッセージ

章のタイトルの通り、本書の具体的な議論は「クラス」から始まるのですが、あくまでオブジェクト指向設計においてはメッセージ(メソッド)が基礎であるということが述べられています。

別の記事で言及しますが、達成すべき要件をもとに設計を考える際にはメッセージを中心に考えることによってどんなクラスが必要になるかが浮かび上がってくるからです。

とりあえず本章でははっきりしている「クラス」から議論を始めています。

模範的なコードの性質は TRUE である

前回の記事でも書きましたが設計は変更容易性を確保するためにあります。その目的を踏まえた模範的なコードは TRUE を満たすと言われています。

  • Transparent(見通しが良い):変更するコードにおいても、そのコードに依存する別の場所のコードにおいても、変更がもたらす影響は明白
  • Reasonable(合理的):変更にかかるコストは変更がもたらす利益にふさわしい
  • Usable(利用性が高い):新しい環境・予測していなかった環境でも再利用できる
  • Exemplary(模範的):コードに変更を加える人が、上記の品質を自然と保つようなコードになっている

この TRUE なコードを書くための第一歩は、それぞれのクラスが明確に定義された単一の責任を持つよう徹底することです。

単一責任が重要な理由

変更が簡単なアプリケーションは、再利用が簡単なクラスから構成されますが、2 つ以上の責任を持つクラスは簡単には再利用はできません。複数の責任を持っているためそのコンテキストに縛られてしまうからです。

また複数の責任を持つクラスは何をするべきかの整理がついておらず、変更が起こる理由が不必要に増えてしまいます。それはそのクラスに依存するクラス全てを壊す可能性を高めます。

単一責任にする

単一責任のクラスを実装するために、以下のポイントを押さえる必要があります。

  • 単一責任かどうかを見極める
  • メソッドも単一責任にする
  • 余計な責任があれば隔離する

単一責任かどうかを見極める

あるクラスが単一責任かどうかを調べるには、クラスの振る舞いが不自然な意味になってないか確かめる方法と 1 文でそのクラスを説明する方法があります。

例えば(自転車の)Gear クラスが単一責任を満たしているかを確かめる際には、そのメソッド名について考えてみます。「Gear さん、あなたのタイヤのサイズを教えてくれませんか?」だと違和感がありますが「Gear さん、あなたの比を教えてくれませんか?」だと理にかなっています。

また、Gear クラスのやっていることを説明する際に「それと」や「または」という単語を使ってしまっていないかをチェックすることも有用です。

メソッドも単一責任にする

メソッドもクラスと同じように、変更と再利用を簡単にするために単一責任であるべきです。

つまりメソッドも役割が何かを説明した際に自然な答えが返ってくるもので、1 文で責任を説明できるようにします。

よくあるパターンが繰り返し処理と繰り返し処理の中で処理を行なっているパターンです。

def diameters
  wheels.collect {|wheel| wheel.rim + (wheel.tire * 2) }
end

上記のメソッドでは wheels を繰り返し処理し、それぞれの wheel の直径を計算しています。

これらのメソッドを責任に沿って分けると以下のようになります。

def diameters
  wheels.collect do |wheel|
    diameter(wheel)
  end
end

def diameter(wheel)
  wheel.rim + (wheel.tire * 2)
end

このようにメソッドを単一責任にすることで、以下のメリットが得られます。

  • 隠されていた性質を明らかにする(クラスが行うことがより明確になる)
  • コメントをする必要がない(その処理のやっていることをコメントする代わりに、メソッド名で示せばいい)
  • 再利用を促進する
  • 他のクラスへの移動が簡単

余計な責任があれば隔離する

メソッドを単一責任にすることのメリットの一つに「隠されていた性質を明らかにする」というものがありました。例えば上に示した diameter(直径)メソッドが Gear クラスにあったらどうでしょう?

「Gearさん、wheel の直径を教えてください」というのは不自然な気がします。Gear が wheel を使って Gear の情報を提供することは問題ない気がしますが、wheel の直径は Wheel クラスのようなものが知っているべきと考えるのが自然ではないでしょうか。

そこで例えば以下のような Wheel クラスを用意すれば、Gear クラスは wheel に関する情報を知らずに利用できます。そのことによって Gear クラスの変更容易性が高まるのです。

class Gear
  attr_reader :chainring, :cog, :wheel

  def initialize(chainring, cog, wheel = nil)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end
end

class Wheel
  attr_reader :rim, :tire

  def initialize(rim, tire)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire * 2)
  end
end

この Gear クラスは wheel が Wheel クラスのインスタンスかどうかは知りません。あくまで diameter メソッドを呼び出せる何らかのオブジェクトだと想定しているだけです。また diameter の計算方法など知る由もありません。これら全てが Gear クラスの不必要な変更を避けることに繋がります。

今回はここまでとなります。オブジェクト指向設計実践ガイドの第 2 章を僕なりにまとめてみました。

Discussion