オブジェクト指向設計ガイド(Ruby)の内容をわかりやすくまとめてみた
Rubyで適切な設計を理解するために必要な知識をまとめたオブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方の書籍を読んだので、内容をまとめます。
ちなみにオブジェクト指向とはそもそも何かという人はオブジェクト指向でなぜつくるのか 第2版を読んでみることをお勧めします。
この本を読むことでオブジェクト指向で開発するメリットからオブジェクト指向が作られた背景までが理解できます。
自分的に大事そうな箇所のみ掲載したので、細かい箇所まで知りたい方はAmazonにて購入お願いします。
第1章「オブジェクト指向設計」
1.1 設計の賞賛
- オブジェクト指向設計とは依存関係を管理することである
- オブジェクト指向ではオブジェクトが変更を許容できるような形で依存関係を構築する
- オブジェクトがお互いを知りすぎていると、一つのオブジェクトを変更した時の他のオブジェクトへの影響範囲が大きくなる
1.2 設計の道具
- オブジェクト指向の設計原則で有名な物にSOLID※1,DRY※2などがある。
※1, 単一責任、オープンクローズド、リスコフの置換、インターフェース分離、依存性逆転のイニシャルをとった言葉
1. Single responsibility principle
2. Open/closed principle
3. Liskov substitution principle
4. Interface segregation principle
5. Dependency inversion principle
※2, Dont Repeat Yourselfの略
1.3 設計の行為
- 設計が失敗する原因は設計が十分でないこと
- 設計を知らなくてもアプリケーションは作れる
- 特にRubyは設計を知らなくても、アプリケーションを作成しやすい
- 設計のないアプリケーションは書くのは簡単だが、次第に変更が出来なくなる
- オブジェクト指向設計とは、変更が簡単になるようなコード設計のことをいう
1.4 オブジェクト指向プログラミングのかんたんな導入
- オブジェクト指向のアプリケーションは、オブジェクトとオブジェクト間で交わされるメッセージから構成されます。
- Rubyはクラスベースのオブジェクト指向言語である
- Rubyではデータと振る舞いをオブジェクトにまとめる
- データへのアクセスをコントロールするのはオブジェクトのみ
- Rubyではクラスを定義できる
- クラスは似たようなオブジェクトの構造の設計図となる
- クラスにはメソッドと属性(変数の定義)を定義する
- メソッドはメッセージに応答する
- Rubyの全てのクラスはClassクラスのインスタンスである
- 故にクラスの拡張が可能になっている
第2章「単一責任のクラスを設計する」
2.1 クラスに属するものを決める
- Rubyのようなクラスベースオブジェクトでは、メソッドはクラス内に定義される
- クラスを作ることは枠組みを作ること
- メソッドを正しくグループ分けして、クラスにまとめることが重要
- 常にコードを簡単に変更できるように保つべき
■ 変更を簡単にするには以下の4つの条件に気をつけるべき
- 1.見通しが良い: 変更をもたらす影響が明白である
- 2.合理的: 変更がもたらす、コストがふさわしい
- 3.利用性が高い: 再利用しやすい
- 4.模範的: コード変更者が上記の品質を自然と保つコードになっている
2.2「単一の責任を持つクラスを作る」
- クラスは可能な限り、最小で有用なことをすべき
- つまり、単一の責任を持つべきです
- 単一の責任を持つアプリケーションは影響範囲が局所的で変更が容易で再利用もし易い
- 逆に2つ以上の責任を持つクラスは、簡単には再利用できない
- 単一の責任を持つアプリケーションを作るにはDRYなコードを意識する
- DRYであれば振る舞いに変更があっても、1箇所のコード変更するだけで実現できる
- メソッドでさえも単一責任に絞るべき(一つのメソッドで一つの責務だけを負わせる) ※1
※1 メソッドを細分化する例
例えば、このメソッドはwheelsを繰り返し処理をして、それぞれのwheelの直径を計算するメソッドです。
def diameters
wheels.collect {|wheel|
wheel.rim + (wheel.tire * 2)}
end
このメソッドで持つ責務は2つあるので、それぞれ分解します。
# 1. 配列を繰り返し処理する
def diameters
wheels.collect {|wheel| diameter(wheel)}
end
# 2. 「一つ」の車輪の直径を計算する
def diameter(wheel)
wheel.rim + (wheel.tire * 2))
end
このようにメソッドレベルでも責務を分解するのが良しとされる。
第3章「依存関係を管理する」
3.1「依存関係を理解する」
- 依存関係と管理し、それぞれのクラスが持つ依存を最低限にすべき
- 依存関係が複雑であれば、再利用などが困難を極める。(影響範囲が大きいため)
- 最終的には一から書き直した方が簡単というような形になってしまう
3.2「疎結合なコードを書く」
■ 依存オブジェクトの注入
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
def gear_inches
ratio * Wheel.diameter
end
後者の場合であれば、Wheelインスタンスの作成をクラス外に移動することでクラスの結合が切り離され、疎結合になります。
■ 脆い外部メッセージを隔離する
def gear_inches
ratio * wheel.diameter
end
def gear_inches
ratio * diameter
end
def diameter
wheel.diameter
end
wheel.diameterを隔離することで依存度を下げることが出来ます。
■ 引数の順番への依存を取り除く
class Gear
attr_reader :chainring, :cog, :wheel
def initialize(args)
@chainring = args[:chainring]
@cog = args[:cog]
@wheel = args[:wheel]
end
end
Gear.new(
:chainring => 52,
:cog => 11,
:wheel => Wheel.new(26, 1.5)).gear_inches
初期化の時にハッシュを引数に利用すると、引数の依存を回避することが出来ます。
この辺りの話はRubyのinitializeメソッドの引数の順番の依存を取り除く方法 にまとめた。
3.3「依存方向の管理」
領域A(抽象領域)
- クラスのうち、変わる可能性はわずかなものの、大量に依存されているものは、領域Aに落ち着く
- この領域に含まれるのは、たいていは抽象クラスやインターフェース
領域B(中立領域)
- 領域Bのクラスは、設計時に最も考慮する必要のないもの
- 潜在的な将来の影響に対しては、ほとんど中立だから
- 滅多に変わることもなく、そこに依存しているものもわずかなもの
領域C(中立領域)
- 領域Cは、領域Aとは真逆
- 領域Cに含まれ るコードはかなり変わりやすく、それでいてそこに依存しているものはわずかしかない
領域D(危険領域)
- 領域Dについては、危険領域と呼ぶのにふさわしい
- 変更が約束され、「かつ」、そこに依存するものも大量にあるとき、クラスは領域Dに陥る
- 領域Dにあるクラスへの変更は、高くつく
領域について
- 領域A、B、Cは、コードがある場所として適正
- 注意しなければならないのは領域D
- 領域Dのクラスが表しているのは、アプリケーションの将来的な健康状態への危険
- 領域Dのコードは一つの変更が他の多くの変更を強要する
- 設計の欠陥は領域Dから生じる
第4章「柔軟なインターフェイスをつくる」
4.1 「インターフェイスを理解する」
上記の画像はオブジェクトとオブジェクト間で受け渡されるメッセージから構成されます。
■ 左のアプリケーション
- 左のアプリケーションは全てのオブジェクトは任意のメッセージにオブジェクトを送れるようになっている
- 左のアプリケーションはそれぞれ自身が外部に晒しすぎている為、左のアプリケーションは再利用が困難
■ 右のアプリケーション
- 右のアプリケーションはメッセージには明確に定義されたパターンがある
- 右のアプリケーションは外部への依存度が低いので、再利用が容易
4.2 「インターフェイスを定義する」
■ パブリックインターフェース
・ クラスの主要な責任を明らかにする
・ 外部から実行されることが想定される
・ 気まぐれに変更されない
・ 他者がそこに依存しても安全
・ テストで完全に文書化されている
■ プライベートインターフェース
・ 実装の詳細に関わる
・ ほかのオブジェクトから送られてくることは想定されていない
・ どんな理由でも変更され得る
・ 他者がそこに依存するのは危険
・ テストでは、言及さえされないこともある(プライベートインターフェースをテストすべきかどうかは様々な意見がある)
ちなみにオブジェクト指向設計ガイドでは、パブリックインターフェースとプライベートインターフェースの関係をUMLを利用して説明している。
4.4 「一番良い面(インターフェース)を表に出すコードを書く」
Rubyにはpublic、protected, privateが存在する
Ruby の private と protected 。歴史と使い分け
4.5 「デメテルの法則」
- デメテルの法則(LoD:Law of Demeter)は、オブジェクトを疎結合にするためのコーディング規則の集まり
- 法則といっても、規則ではないのでたまに破ってしまうこともしばしばある
- デメテルは、そこへメッセージを「送る」ことができるオブジェクトの集合を制限する
第5章「ダックタイピングでコストを削減する」
- ダックタイプはいかなる特定 のクラスとも結びつかないパブリックインターフェースです。
- クラスをまたぐインターフェースである
- ダックタイピングを使いこなす事で、アプリケーションに柔軟性を持たせる事ができる
5.1「ダックタイピングを理解する」
- ダックタイピングではオブジェクトが何であるかではなく何をするかが重要
- ダックタイプを使うことで、コードは具象的なものからより抽象的なものへと変わっていきます。拡張はよりかんたんになるものの、ダックの根底にあるクラスは覆い隠される。
■ ダックタイピングの概要については【OOP入門】Rubyでダックタイピングを理解するが参考になる。
5.2「ダックを信頼するコードを書く」
- ダックに書き換えられるコードは多くある
- 次のものはダックに書き換える事が出来る
- クラスで分岐するcase文
- kind_of?とis_a?
- responds_to?
■ クラスで分岐するcase文の例
class Trip
attr_reader :bicycles, :customers, :vehicle
def prepare(preparers)
preparers.each {|preparer|
case preparer
when Mechanic
preparer.prepare_bicycles(bicycles)
when TripCoordinator
preparer.buy_food(customers)
when Driver
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
}
end
end
■ kind_of?とis_a?
if preparer.kind_of?(Mechanic)
preparer.prepare_bicycles(bicycle)
elsif preparer.kind_of?(TripCoordinator)
preparer.buy_food(customers)
elsif preparer.kind_of?(Driver)
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
■ responds_to?
if preparer.responds_to?(:prepare_bicycles)
preparer.prepare_bicycles(bicycle)
elsif preparer.responds_to?(:buy_food)
preparer.buy_food(customers)
elsif preparer.responds_to?(:gas_up)
preparer.gas_up(vehicle)
preparer.fill_water_tank(vehicle)
end
第6章「継承によって振る舞いを獲得する」
- クラスによる継承は、サブクラスを作ることによって定義される
- 継承によって、共有されるコードを 隔離でき、そして共通のアルゴリズムを抽象クラスに実装できる
- 抽象的なスーパークラスをつくるための一番良い方法は、具象的なサブクラスからコードを押し上げること
6.2「継承を使うべき箇所を識別する」
- Rubyでは単一継承を採用している
- サブクラスは一つのスーパークラスしか持つことができない
- クラスによる継承を利用したメッセージのやり取りはクラス間で行われる
- 第5章で学んだダックタイピングはクラスを横断するもの
- オブジェクトが理解できないメッセージを送った時はスーパークラスに探しにいく
■ 画像参考: オブジェクト指向設計ガイド page150
6.3「継承を不適切に適用する」
- superを利用すると、そのメッセージがスーパークラスのチェーンを上って渡されます
参考記事:スーパークラスのメソッドを呼び出す
6.4「抽象を見つける」
- サブクラスはスーパークラスを特化したもの
- 共通の振る舞いを持つ場合はスーパークラスにメッセージを書く
■ スーパークラスは抽象的になる
Bicycleが共通の振る舞いを持ち、MountainBikeとRoadBikeがそれぞれ特化した振る舞いを持つ。
参考図のようにBicycleクラスを作成しないと、大量の重複したコードを持つMountainBikeと RoadBikeクラスを書くことになる。
■ 画像参考: オブジェクト指向設計ガイド page154
第7章「モジュールでロールの振る舞いを共有する」
7.1「ロールを理解する」
- 共通な振る舞いを持つロールはモジュール化させる
- Rubyのクラスは、他の言語でいう多重継承を基本的に許さず、そのかわりにモジュールを利用する
- Rubyのincludeキーワードを使ってモジュールを「クラス」にインクルードする
- Rubyのextendキーワードを使うと、オブジェクト1つだけにモジュールのメソッド を追加することもできる(クラスメソッド)
■ ざっくりしたメソッド探索流れ(モジュールない場合)
1. sparesメッセージがMountainBikeのインスタンスに送られる
2. まず、MoutainBikeクラスの中にaparesメソッドがないか探索します。
3. MoutainBikeクラスに見つからない場合はスーパークラスのBicycleクラスに探索します
4. もし、Bicycleクラスにもない場合は断層構造の頂点のObjectクラスまで探しに行きます
■ ざっくりしたメソッド探索流れ(モジュールある場合)
1. sparesメッセージがMountainBikeのインスタンスに送られる
2. まず、MoutainBikeクラスの中にaparesメソッドがないか探索します。
3. MoutainBikeクラスに見つからない場合はスーパークラスのBicycleクラスに探索します
4. もし、Bicycleクラスにもない場合はBicycleクラスにincludeされているSchedulableモジュールを探しに行きます
5. Schedulableモジュールにもない場合は断層構造の頂点のObjectクラスまで探しに行きます
7.2「継承可能なコードを書く」
- オブジェクト指向のRubyでは断層構造を自由自在に作成することができます。
1. 広く浅い断層構造は簡単に理解できる
2. 浅く広くはそれより若干複雑
3. 深く狭いはもう少し難しい、プラス幅も広くなりがち
4. 深く広くはかなり複雑なので、アンチパターン
まとめ
個人的感想
■ Positive
- Rubyのオブジェクト指向に絞って話していたので、Rubyの設計構造について詳しくなれた
- クラス設計について理解を深めることが出来た
■ Negative
- 日本語訳がわかりづらい箇所が多かった
- 抽象的な表現が多くわかりづらい
以上!
もっと詳細内容が知りたい方はAmazonにて購入してください!