【無職に転生 ~ 就職するまで毎日ブログ出す_17】【書籍】オブジェクト思考設計実践ガイド 2章 - 4章までざっくりと
はじめに
こんにちは、大ちゃんの駆け出し技術ブログです。
タイトルにあるとおり【無職に転生 ~ 就職するまで毎日ブログ出す】というチャレンジをしています!!!!昨日までは就活するまで本気出すでしたが、これだとまるで就活後は頑張らないのかと思われてしまいそうで、、、大人気アニメのタイトルのまるパクリチャレンジです。
RailsやらRubyやらSQLやらその他Webの知識やらが色々と抜け落ちているのを感じており、知識の定着のためにもアウトプットする機会を増やすためです。加えて、退職して文字通り無職に転生しましてプロニートになり、毎日時間に余裕ができたので引き締めるためにも毎日投稿を思い至りました!
【投稿内容】
- SQLの難しい処理 (副問合せ、JOINとか複雑な処理が書けない)
- Rails全般 (純粋に必要な知識が多すぎる、網羅的な理解が足りない)
- Rubyのあまり使わないメソッドや記述方法 (あまり重要ではないけど特に)
- Web知識全般 (クッキーやら、セッションやらなんとなくで理解しているものの自分の言葉で説明できない)
- 書籍 (スタートアップ企業に勤めるので、自分が会社に与える影響やパフォーマンスを高めるためビジネス書を読んでいきます。)
本日やること
本日は書籍「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」という本を4章までざっくりとまとめました。
2 単一責任を理解する
オブジェクト指向設計実践ガイドを読む No.2【単一責任のクラスを設計する】
変更が簡単なコードを組成
変更が簡単なコードとは以下の性質を持つ
- 見通しが良い (変更がもたらす影響・変更箇所が明確にわかる)
- 合理性 (如何なる変更でもかかるコストは変更がもたらす利益の割にあっている)
- 利用性が高い (新しい環境、予期していなかった環境でも再利用できる)
- 模範的 (コードに変更を加える人が上記の品質を自然と保つようなコードになっている)
上記の性質を満たすための第一歩
⇒ それぞれのクラスが明確に定義された単一の責任を持つように徹底すること
なぜ単一責任が重要なのか
別のクラスとの依存性がわずかしかないため、単独で変更した時に他のクラスへの影響がほとんどないため
クラス(メソッド)が単一責任かどうか見極める方法
- あたかもそれに知覚があると仮定し質問する
「Gearクラスさん、あなたの比率を教えてくれませんか?」→ ○
「Gearクラスさん、あなたのタイヤのサイズを教えてくれませんか?」→ ❌
- 一文でクラスを説明してみる
「Gearクラスはギアの比率を教える責任がある」 → ○
「Gearクラスはギアの比率を教える責任がある。それと、タイヤのサイズを教える責任がある。」→ ❌
⇒「それと」や「または」が含まれていれば、それは単一責任から外れている
3. 依存関係を管理する
- 複雑な処理があればあるほど、2で学習した単一責任のクラス、メソッドが複雑に絡み合って、依存関係を生み出していく
依存関係があるということはオブジェクトがその関係にあるオブジェクトのことを知っているということ
依存関係を理解する
- 一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更しなければならない時がある
サンプルコード
class Gear attr_reader :chainring, :cog, :rim, :tire def initialize(chainring, cog, rim, tire) @chainring = chainring @cog = cog @rim = rim @tire = tire end def gear_inches ratio * Wheel.new(rim, tire).diameter end def ratio chainring / cog.to_f end end class Wheel attr_reader :rim, :tire def initalize(rim, tire) @rim = rim @tire = tire end def diameter rim + (tire * 2) end end Gear.new(52, 11,26, 1.5).gear_inches
オブジェクトが次のものを知っている時、オブジェクトには依存関係がある
- 他のクラスの名前:GearはWheelという名前のクラスが存在することを予想している
- self以外の何処かに送ろうとするメッセージの名前:GearはWheelのインスタンスがdiameterに応答することを予想している
- メッセージが要求する引数:GearはWheel.newにrimとtireが必要なことを知っている
- それらの引数の順番:GearはWheel.newの最初の引数がrimで、2番目がtireである必要があることを知っている
ratio * Wheel.new(rim, tire).diameter
設計課題:依存関係を管理しそれぞれのクラスが持つ依存を最低限にすること
疎結合なコードを書く
依存オブジェクトの注入
以下の場合にGearクラスを変更しなければならない ・Wheelクラスの名前をWheelyにした時 ・gear_inchesメソッドにディスクやシリンダという別の直径の値を反映させたい時
改善!
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def gear_inches ratio * wheel.diameter end def ratio chainring / cog.to_f end end ・ ・ Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
・GearはWheelというオブジェクトの名前を知らない (wheelメソッドでアクセウするようにしている) ・他の直径を計測する必要があるオブジェクトにもGearクラスを変えることなしに対応できる
Gearは知っていることが少なくなったおかげで、賢くなった
依存を隔離
外部メッセージを隔離
外部メッセージ = 「self以外に送られるメッセージ」
diameter
は外部のクラスを参照する(外部にメッセージが送られる)
def gear_inches ratio * wheel.diameter end
↓
def gear_inches ratio * diameter end def diameter wheel.diameter end
メソッドとして切り出すことで、レシーバーであるwheelの変更に対して柔軟になる
引数の順番への依存を取り除く
- 引数ある場合、引数を渡す順番に依存してしまうため、その依存をなくし順番からの依存をなくす
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def gear_inches ratio * wheel.diameter end def ratio chainring / cog.to_f end end Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
newメソッドの順番が定義しているinitializeメソッドによって固定されてしまい、送り手(メソッドを呼び出す側)が順番に依存して引数を渡す必要がある ⇒ initializeメソッドの引数の順番が変われば、送り手も変えなければならない
↓
初期化の際に引数にキーワード引数を使う
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring: "52", cog: "11", wheel: "10") @chainring = chainring @cog = cog @wheel = wheel end def gear_inches ratio * wheel.diameter end def ratio chainring / cog.to_f end end
Gear.new(chainring: 52, cog: 11, wheel: Wheel.new(26, 1.5)).gear_inches Gear.new(cog: 11, wheel: Wheel.new(26, 1.5), chainring: 52).gear_inches
- 引数の依存から逃れることができる
参考記事
Rubyのキーワード引数の使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン
Rubyのキーワード引数はシンボルっぽく定義するけど、シンボルそのものではない、という話 - Qiita
依存方向の管理
オブジェクト指向設計実践ガイドを読む No.3【依存関係を管理する】
依存関係には常に方向性がある
依存関係の逆転
これまでの例はGearがWheelまたはdiameterに依存している
Wheelの仕様に変更があればGearはそれに合わせて仕様を変えなければいけない場合がある
⇒ これを反対にWheelがGearに依存するようにする (依存関係の逆転)
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog) @chainring = chainring @cog = cog end def gear_inches(diameter) ratio * diameter end def ratio chainring / cog.to_f end end class Wheel attr_reader :rim, :tire, :gear def initalize(rim, tire, gear) @rim = rim @tire = tire @gear = Gear.new end def diameter rim + (tire * 2) end def gear_inches gear.gear_inches(tire * 2) end end Wheel.new(26, 1.5, Gear.new(52, 11)).gear_inches
Gearの仕様に変更があればWheelはそれに合わせて仕様を変えなければいけない場合がある
- メリットがある場合がある(上記の例ではメリットはない)
- アプリケーションが大きくなるにつれて依存方向の選択が重要になる
依存方向の選択
クラスがあたかも人間であるように考え、クラスにアドバイスをするとするならば、次のようにアドバイスするのではないか
**「自信より変更されないものに依存しなさい」 (自信よりも変更される可能性が多いクラスに依存すると自身が変わる回数が多くなる)**
- あるクラスは他のクラスよりも要件が変わりやすい (理解!)
- 具象クラスは抽象クラスよりも変わるかのせいが高い (理解!)
- 多くのところから依存されたクラスを変更すると、広範囲に影響が及ぶ (理解!)
変更の起きやすさを理解する
例
変わりにくいコード
- プラグインやgemのコード
- 既存のメソッド、クラス (spliceメソッド、Arrayクラス)
変わりやすいコード
- 自分が書いたコード
コードの中で変わりやすいコードと変わりにくいコードを順位づけることが、依存関係を決める上で重要な鍵になる なぜなら、変わりやすいコードが依存するのはそれよりも変わりにくいコードであるほうがよいため
具象と抽象を認識する
依存オブジェクトの注入の際に、GearのWheelクラスの具象性(具体的な引数)をwheelに置き換えることで、他のクラスを知っていることをなくして抽象化した
class Gear attr_reader :chainring, :cog, :wheel def initialize(chainring, cog, wheel) @chainring = chainring @cog = cog @wheel = wheel end def gear_inches ratio * wheel.diameter end def ratio chainring / cog.to_f end end ・ ・ Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches
変更前が具象、変更後は抽象
メリット:変わりにくいオブジェクトになる
⇒ 変わりにくい変わりやすいの性質が上下する
大量に依存されたクラスを避ける
シンプルに多くから依存されたクラスを持てばそれを変更した時依存されているクラスの数だけコードを修正する必要がある
問題となる依存関係を見つける
依存関係を決める上で、重要になる尺度
- クラスの変わりやすさ
- 依存されている数
適切に設計されたアプリケーションはDの危険領域にあたるクラスがない
D = 変わりやすく多くのクラスに依存されている
4.柔軟なインターフェースを作る
オブジェクト指向設計実践ガイドを読む No.4【柔軟なインターフェースをつくる】その 1
オブジェクト指向設計実践ガイドを読む No.4【柔軟なインターフェースをつくる】その 2
インターフェースを定義する
(レストランの例え話)
お客 ⇒ 具体的な作り方を知らないままメニューを見て頼む (何が作れるのかは知っている)
コック ⇒ 具体的な作り方を独自で知っている
上記の例で言うと、メニューは公開されている情報 (パブリック) public
作り方は公開されていない情報 (プライベート) private
パブリックインターフェースとプライベートインターフェス
パブリックインターフェースのメソッドの特徴
- クラスの主要な責任を明らかにする (クラスの責任を明確に述べる契約書)
- 外部から実行されることが想定される
- 気まぐれに変更されない
- 他者がそこに依存しても安全
- テストで完全に文書化される
プライベートインターフェスのメソッドの特徴
- 実際の詳細に関わる
- 他のオブジェクトから送られてくることは想定されない
- どんな理由でも変更されうる
- 他者がそこに依存するのは危険
- テストでは言及されないこともある
パブリックインターフェースとプライベートインターフェースの例
シチュエーション: ・ある旅行(Trip)では必ず必要な自転車(bicycle)が整備されている必要がある ・整備士(Mechanic)が自転車を整備する
class Trip has_many: bicycles def clean_bicycle @mechanic = Mechanic.new(name: hoge) bicycles.each do |bicycle| @mechanic.clean_bicycle(bicycle) @mechanic.pump_tires(bicycle) @mechanic.lube_chain(bicycle) @mechanic.check_brakes(bicycle) end end end
class Mechanic def clean_bicycle(bicycle) end def pump_tires(bicycle) end def lube_chain(bicycle) end def check_brakes(bicycle) end private end
上記では以下の問題点がある
- TripクラスがMechanicクラスの多くのインターフェースを知っている ⇒ Mechanicクラスを変更するとTripクラスが大きく変更される可能性がある
- つまり、Tripクラスはメニューだけでなく、作り方も全部知ってしまっている(厨房が筒抜け)
⇒リファクタリング
class Trip has_many: bicycles def clean_bicycle @mechanic = Mechanic.new(name: hoge) bicycles.each do |bicycle| @mechanic.prepare_bicycle(bicycle) end end end
class Mechanic def prepare_bicycle(bicycle) clean_bicycle(bicycle) pump_tires(bicycle) lube_chain(bicycle) check_brakes(bicycle) end private def clean_bicycle(bicycle) end def pump_tires(bicycle) end def lube_chain(bicycle) end def check_brakes(bicycle) end end
- Tripクラスはパブリックインターフェースを少ししか知らない状態 ⇒ Mechanicクラスに変更が起きてもTripクラスに変更は起きにくい