【Ruby】オブジェクト思考設計実践ガイドを読み始めた
オブジェクト思考設計ガイドを読み始めた
記事に書くこと
こちらの本を2章を読んだことの忘備録になります。
読み始めた背景
設計て大事ですよね。
なるべく早くアプリケーション開発するのは大事ですが、
設計がボロボロだった時に拡張ができないとか問題が発生します。
RubyとかRailsを少しは書けるようになってきましたが、
実装以外で迷うところはやはり設計です。
どこにコードを書くのか?、保守性が高いコードを書くためにはどのようなModelにするべきか、かなり迷います。
そんなこんなで、設計力を高めていきたいなと思いこちらの本を購入しました。
第2章 単一責任のクラスを設計する
先にまとめておくと
- クラスは今すぐに求められる動作を使い、後にも簡単に変更できるようにモデル化する
- そのためにメソッドを正しくグループ分けしてクラスにまとめることが必要
- 単一責任のクラスを作る
- クラスを変更する理由は2つ以上あってはならない
- クラスを1文で説明して、
and
とかor
がついたら複数の機能を持っている
- データを外部から隠蔽する
- データとしてではなく、アクセサメソッドを介して、外部から参照できるようにする
変更が簡単なコード
変更が簡単なコードって難しいですよね。
そもそも変更が簡単なコードって何だよと私もすごい思います。
書籍では変更が簡単
について以下のように定義されていました。
- 変更が副作用をもたらさない
- 要件の変更範囲とコードの変更規模は比例する
- 少しだけ変更するだけだったら、コードを変更する箇所も少しだよね的な
- 既存のコードは簡単に利用できる
- 何か追加の機能を開発する時に既存のコードを流用できるとか
- Railsだと
scope
とかが該当するのではないかと
- 最も簡単な変更方法はコードの追加である。ただし追加するコード自体は変更が容易なものとする
- 機能改修が多く、私もコードを追加することが多いのでとても染みた
じゃあ、上の定義を実現するためにどんなコードを書かないといけないのか?
下のTRUE(頭文字をとって)
がコードがあるべき姿になります。
- TransParent(見通しがいい)
- 変更がもたらす影響が明白であること
- コードが依存している場所、依存されている場所は明白であること
- Reasonable(合理的)
- どんな変更であっても、かかるコストは変更がもたらす利益に等しい
- Usable(利用性が高い)
- 新しい環境、予期していなかった環境でも利用できる
- Exemplary(模範的)
- コードに変更を加える人が、上記の品質を自然と保つようなコードになっている
- ある意味コードが、コーティング規約になっているみたいな感じ
TRUE
なコードを書くための最初の1歩が単一責任
を実現することになります。
単一責任って何?
オブジェクト思考とかで、よく言われる単一責任の原則
とかってありますよね。
単一責任
とは、
1つのクラスに1つの役割(機能)
と言うものになります。
1つのクラスに1つの役割って解釈がとても難しいですよね。
システムAとシステムBがあり、そこをつなげるクラスを作成していたとします。
作ったクラスが下のような、機能を持っている時これは単一責任でしょうか?
システムAから何かしらの命令を受けてそれをシステムBが処理できる形に変換する
変換した命令をシステムBへ通知する
システムAとシステムBの橋渡し(機能)と考えた場合って、単一責任
ですよね。
ただ、オブジェクト思考的には、これは単一責任とは言えません。
作成したクラスは、システムAから命令を受けて変換する機能
とシステムBに対して通知する機能
2つの機能が混在しているためです。
じゃあ、どうやってクラスが単一責任かチェックするのか。
作成したクラスを変更する理由が2つ以上存在してはならない
と言う意味に置き換えるてクラスの妥当性をチェックしていきます。
この意味で考えると、上のクラスが変更する理由としては、下のようなものです。
つまり、システムAから命令を受けて変換する機能
とシステムBに対して通知する機能
は
それぞれ別クラス(もしくはインタフェースを作成するなど)に分離する必要があります。
役割=クラスを変更する理由
と考えることで、単一責任になっているかを考えていくことができます。
なぜ単一責任が必要になるのか?
単一責任は、再利用が簡単なコードを書くための原則です。
2つ以上の責任を持っている場合、簡単に再利用ができなくなります。
(ある一方の機能のみ欲しいんだけど。。とりあえず別クラスに複製してみるかみたいな感じになりそうですよね)
必要な機能(振る舞い)のみで構成されているクラスは、クラス全体を複製することができるのです。
既存のコードが単一責任であるかどうかについて見極めるもう1つの方法としては、
コードの役割を1文で説明できるか
と言うことを意識してもいいようです。
説明に、それと
が含まれている場合は、2つ以上の責任を持っています。
または
がつく場合はそこまで関連していない2つ以上の責任を負っていると言うことになります。
データではなく振る舞いに依存させる
データ=クラスがもつ変数
、振る舞い=メソッド
です。
単一責任のクラスを作れば、どんな些細な振る舞いの変更も、
ただ1箇所のみに存在するようになります。
これがDRY
の概念とも被ります。
DRY
なコードは振る舞いに変更があったとしても、変更する箇所はただ1箇所になります。
メソッドもデータにアクセスする必要があります。
データにアクセスするための方法は、
があります。
業務要件として、データに直接アクセスする必要がある場合もありますが、
大抵の場合は、データをオブジェクトとして扱い、外部に対しては隠蔽
し、
外部からはアクセサメソッド経由でアクセス
する方が望ましいものとなります。
class Gear # Rubyのattr_readerでインスタンスパラメータをカプセル化(読み取り専用) attr_reader :chainring, :cog def initialize(chainring, cog) @chainring = chainring @cog = cog end def ratio chainring / cog.to_f end end # attr_readerは下のメソッドを定義したと同じ def cog @cog end # 外部からはデータを直接書き換えることはできない gear = Gear.new(10, 10) gear.cog = 20 # NG
Structを使ったデータの隠蔽
下のようなクラスとメソッドがあるとします。
Gearは外部から2次元配列をもらってインスタンス化されます。
class Gear attr_reader :data def initialize(data) @data = data end def diameters # 配列のindexを通してデータを処理 data.collect {|cell| cell[0] + (cell[1] * 2)} end # ここからも先のメソッドも配列のindexを通してデータを処理する # .... end data = [ [10, 20], [20, 30], [30, 40] ] gear = Gear.new(data)
dataはattr_reader
で確かに隠蔽されていますが、
data自体が複雑な構成になっていて、diametersメソッドは、
dataの構造に依存した処理になっています。
配列の構造が変わった瞬間に壊れる可能性があるコードです。
配列の構造を知っている場所をただ1箇所に抑え込む必要があります。
そんな時に構造体Struct
を使ってみます。
class Gear attr_reader :data def initialize(data) @wheels = wheelify(data) end def diameters @wheels.collect {|wheel| wheel.rim + (wheel.tire * 2)} end Wheel = Struct.new(:rim, :tire) def wheelify(data) data.collect {|cell| Wheel.new(data[0], data[1])} end end
配列の構造を知っている場所が、wheelify
だけになりました。
他のメソッドからWheelオブジェクトを介して、振る舞いが実装されています。
配列の構造が変わったとしても、wheelify
のみ変更すればいいコードになっています。
メソッドも単一責任にする
クラスと同様にメソッドも単一責任にすることで、
再利用しやすいものになります。
先ほどのコードのdiameters
メソッドに着眼します。
def diameters @wheels.collect {|wheel| wheel.rim + (wheel.tire * 2)} end
diameters
メソッドは、配列で持っているタイヤを順番に直径計算して、
直径の配列を返すメソッドになります。
直径の配列を返す
と直径を計算する
複数の機能を持っています。
直径計算を別なメソッドに切り出して、diameters
は下のようにリファクタリングされます。
def diameters @wheels.collect {|wheel| diamter(wheel.rim, wheel.tire)} end def diameter(rim, tire) rim + (tire * 2) end
クラス内の余計な責任を隔離する
先ほどStruct
を使って、Wheelを実現していました。
ただ、このモデル設計は違和感があります。
Gear
クラスを一文で説明した際に、
外部からデータをもらってWheelを生成する。それと、wheelの直径を計算して・・・
Gearの目的とは離れた役割が出てきました。
wheel
は外部にクラスとして分離していきます。
最終的には下のような構成にすることで、単一責任を実現しています。
class Gear attr_reader :chainring, :cog, :data def initialize(chainring, cog, wheel = nil) @chainring = chainring @cog = cog @wheels = 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 def circumference diameter * Math::PI end end @wheel = Wheel.new(10, 1.5) @gear = Gear.new(52, 11, @diameter)
所感
今回のケースは、メッセージを取り合うクラスが2つしか存在しないので、
とてもシンプルな例だったと思います。
実際のアプリはもっと多くのクラスと依存関係をもちメッセージパッシングするので、
いつの間にか複数の役割を持っていたりします。
3章はその依存関係について記載されているみたいです。
(ここら辺の読書記録は2日に1回ペースであげたいけどなかなかできていない)