大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【無職に転生 ~ 就職するまで毎日ブログ出す_17】【書籍】オブジェクト思考設計実践ガイド 2章 - 4章までざっくりと

はじめに

こんにちは、大ちゃんの駆け出し技術ブログです。

タイトルにあるとおり【無職に転生 ~ 就職するまで毎日ブログ出す】というチャレンジをしています!!!!昨日までは就活するまで本気出すでしたが、これだとまるで就活後は頑張らないのかと思われてしまいそうで、、、大人気アニメのタイトルのまるパクリチャレンジです。

https://i.gyazo.com/3a02b7aae4e5e538130d4bb199b55dc7.png

RailsやらRubyやらSQLやらその他Webの知識やらが色々と抜け落ちているのを感じており、知識の定着のためにもアウトプットする機会を増やすためです。加えて、退職して文字通り無職に転生しましてプロニートになり、毎日時間に余裕ができたので引き締めるためにも毎日投稿を思い至りました!

【投稿内容】

  • SQLの難しい処理 (副問合せ、JOINとか複雑な処理が書けない)
  • Rails全般 (純粋に必要な知識が多すぎる、網羅的な理解が足りない)
  • Rubyのあまり使わないメソッドや記述方法 (あまり重要ではないけど特に)
  • Web知識全般 (クッキーやら、セッションやらなんとなくで理解しているものの自分の言葉で説明できない)
  • 書籍 (スタートアップ企業に勤めるので、自分が会社に与える影響やパフォーマンスを高めるためビジネス書を読んでいきます。)

本日やること

本日は書籍「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」という本を4章までざっくりとまとめました。

2 単一責任を理解する

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

変更が簡単なコードを組成

変更が簡単なコードとは以下の性質を持つ

  • 見通しが良い (変更がもたらす影響・変更箇所が明確にわかる)
  • 合理性 (如何なる変更でもかかるコストは変更がもたらす利益の割にあっている)
  • 利用性が高い (新しい環境、予期していなかった環境でも再利用できる)
  • 模範的 (コードに変更を加える人が上記の品質を自然と保つようなコードになっている)

上記の性質を満たすための第一歩

⇒ それぞれのクラスが明確に定義された単一の責任を持つように徹底すること


なぜ単一責任が重要なのか

別のクラスとの依存性がわずかしかないため、単独で変更した時に他のクラスへの影響がほとんどないため

クラス(メソッド)が単一責任かどうか見極める方法

  • あたかもそれに知覚があると仮定し質問する

「Gearクラスさん、あなたの比率を教えてくれませんか?」→ ○

「Gearクラスさん、あなたのタイヤのサイズを教えてくれませんか?」→ ❌

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

「Gearクラスはギアの比率を教える責任がある」 → ○

「Gearクラスはギアの比率を教える責任がある。それと、タイヤのサイズを教える責任がある。」→ ❌

⇒「それと」や「または」が含まれていれば、それは単一責任から外れている


3. 依存関係を管理する

  • 複雑な処理があればあるほど、2で学習した単一責任のクラス、メソッドが複雑に絡み合って、依存関係を生み出していく
  • 依存関係があるということはオブジェクトがその関係にあるオブジェクトのことを知っているということ

    Untitled

依存関係を理解する

  • 一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更しなければならない時がある

サンプルコード

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 = 変わりやすく多くのクラスに依存されている

https://i.gyazo.com/b5a9392f8889dd9bb4319287bf683035.png

4.柔軟なインターフェースを作る

オブジェクト指向設計実践ガイドを読む No.4【柔軟なインターフェースをつくる】その 1

オブジェクト指向設計実践ガイドを読む No.4【柔軟なインターフェースをつくる】その 2

https://slidesplayer.net/slide/11190418/60/images/3/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E6%A6%82%E8%A6%81+%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0+B-%EF%BC%91+C-%EF%BC%91+A-1+C-%EF%BC%92+D-1.jpg

インターフェースを定義する

(レストランの例え話)

お客 ⇒ 具体的な作り方を知らないままメニューを見て頼む (何が作れるのかは知っている)

コック ⇒ 具体的な作り方を独自で知っている

上記の例で言うと、メニューは公開されている情報 (パブリック) 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クラスに変更は起きにくい