新宿(近々代々木に移転)で働く18歳エンジニアのブログ

思ったこととか、技術的なこと書きます。

(2) 単一責任のクラスを設計する

TRUE 変更が簡単なコードに伴う性質

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

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

クラスが単一責任かどうか見極める方法

  • あたかもそれに知覚があるかのように問いただすこと。

    クラスのメソッドを質問に置き換えた時に意味をなす質問になっているか。 : Gearさん、あなたの比(ratio)を教えてくれませんか? → OK Gearさん、あなたのgear_inchesを教えてくれませんか? → NG gear_inchesは自転車の情報なので、gearに聞くのは筋違いではないか。 Gearさん、あなたのタイヤのサイズを教えてくれませんか? → NG タイヤのサイズは自転車の問題なのではないか。 Gearさん、あなたのタイヤ(tire)はなんですか?→ OK タイヤがgearの属性になっているから

  • 一文でクラスを説明してみること

    考えつく限り短い説明に「それと」が含まれていれば、おそらくクラスは二つ以上の責任を負っている。「または」が含まれていれば、二つ以上責任があるわけではなく、互いにあまり関連もしない責任を負っている。

  • 凝縮度

    クラス内の全てがそのクラスの中心的な目的に関連していれば、このクラスは凝縮度が高い、もしくは単一責任であると言われる。単一責任の原則(RDD)はRDD(Responsibility Driven Design)という概念に由来。「クラスはその目的を果たすための複数の責任を持つ」

設計を決定するときを見極める方法

今日何もしないことの将来的なコストはどれだけだろうと考える。 とても小さなアプリケーション、未来は不確か、初期段階での知識量はプロジェクト全体でいえばもっとも少ない。最も費用対効果の高い行動は「より多くの情報を持つこと」。

何もしないことによる将来的なコストがいまと変わらないときは、決定は延期する。決定は必要なときにのみその時点で持っている情報を使ってする。

現時点での要件と未来の可能性の相互間のトレードオフをよく理解し、コストが最小になるように決断を下す。

変更を歓迎するコードを書く

振る舞いはメソッド内に捉えられていて、メッセージを送ることによって実行できる。 →単一責任のクラスをつくれば、どんな些細な振る舞いもそれぞれがただ一つに存在 = DRY

オブジェクトは、振る舞いに加えデータを持ち、インスタンス変数に保持される。

インスタンス変数の隠蔽

データへのアクセスは、インスタンス変数を直接参照するのか・隠蔽するのかの二つのうちどちらかで行われるが、インスタンス変数は常にアクセサメソッドで包み、直接参照しないようにする。

  • Module#attr_reader

    Creates instance variables and corresponding methods that return the value of each instance variable. Equivalent to calling “attr:name” on each name in turn. String arguments are converted to symbols.

# attr_reader :hoge
def hoge
  @hoge
end

cogメソッドはコード内で唯一cogが何を意味するのかをわかっている箇所。 コグは、データ(どこからでも参照される)から振る舞い(一箇所で定義される)へと変わる。

デメリット

  • 可視性、publicなcogメソッドを定義すると、クラス外のアプリケーション内の他のオブジェクトにも公開される。またprivateなcogメソッドの場合は、その振る舞いをアプリケーション全体に公開せずに済む。
  • データも普通のオブジェクトも一見区別ができなくなる。

データ構造の隠蔽

インスタンス変数に結びついていることがよくないのであれば、複雑なデータ構造への依存はさらによくない。データ構造に関する複雑な情報は隔離されるべき。

  • obscuring_references.rb dataメソッドは単に配列を返すだけ。dataメッセージの送り手それぞれが何のデータが配列のどのインデックスにあるかを完全に知っていなければならない。 またdiametersメソッドが知っているのは、直径を計算する方法だけではなく、リムとタイヤのがどこにあるかも知っている。→配列の構造に依存しているため、配列にデータを持つとすぐにあちこちで配列の構造に参照するようになる

  • revealing_references.rb diametersメソッドは配列の内部構造について何も知らない。 渡されてくる配列の構造に関する知識はすべてwheelifyメソッド内に隔離。wheelifyメソッドは、arrayの配列をStructの配列に変換。

Struct
[明示的にクラスを書くことなく、いくつもの属性を1ヶ所に束ねるための便利な方法。](http://ruby-doc.org/core-2.4.1/Struct.html)

メソッドから余計な責任を抽出する

メソッドもクラスのように単一の責任を持つべき

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

wheelの繰り返し処理と、wheelの直径の計算、二つ責任を持っている。

def diameters
  wheels.collect {|wheel| diameter(wheel)}
end

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

それぞれの要素に対し実行される処理から、繰り返しを分離することはよくある

def gear_inches
  ratio * (rim + (tire * 2))
end

車輪の直径の計算とgear_inchesの計算の二つ責任を持っている。

def gear_inches
  ratio * diameter
end

def diameter
  rim + (tire * 2)
end

最終的な設計がわかっていない段階でもするべき

単一責任のメソッドがもたらす恩恵

  • 隠蔽されていた性質を明らかにする
    • クラスをリファクタリングし、全てのメソッドが単一の責任を持つようにするとクラスが明確になる
  • コメントをする必要がない
    • そのメソッド内のコードにコメントが必要なくらいなら、そのコードを別のメソッドに抽出する
  • 再利用を促進する
  • 他のクラスの移動が簡単

クラス内の余計な責任を隔離する

Gearクラスにはいくつか車輪(wheel)のような振る舞いがある ではこのアプリケーションにWheelクラスは必要なのだろうか?

いろいろな場合が選択肢にある。

でも目的は、設計に手を加える数を可能な限り最小にしつつ、Gearを単一責任に保つこと

変更可能なコードを書いているので、どうしてもしなければならないときまで決断を先延ばす。

WheelをGearに組み込むことは長期的な設計目標でないことは明らか Gearをきれいにしながらもwheelに関する決定は遅らせている。

その後、自転車の車輪の円周もほしいと言われる。=WheelクラスをGearから独立させて使いたいという明確なニーズ Wheelの振る舞いをGearクラス内に隔離しておいたおかげでこの変更は難しくない。Wheel Structを独立したWheelクラスに変えて円周を計算するcircumstancesメソッドを追加するだけ

まとめ

ひとつのことに専念するクラス(単一責任のクラス)はその一つのことをアプリケーションの他の部位から隔離することで、悪影響を及ぼすことのない変更と重複のない再利用が可能になる

単一責任のクラスにするためにはそれぞれのメソッドも単一責任である必要がある

単一責任のクラスかどうか見極める方法

  • [ ] あたかもそれに知覚があるかのように問いただすこと。
  • [ ] 一文でクラスを説明してみること

設計を決定するときは、現時点での要件と未来の可能性の相互間のトレードオフをよく理解し、コストが最小になるように決断を下す。

また変更を歓迎するコードを書くためには以下が必要

メソッドからも余計な責任を抽出する(それぞれの要素に対し実行される処理から、繰り返しを分離する)

クラス内の余計な責任を隔離する

決定は必要になったときにのみ、その時点で持っている情報を使ってする。

参考リンク