WIP

[読書メモ]オブジェクト指向設計実践ガイド

created at 2017.01.03
updated at 9ヶ月前 ago
vsanna / public Programming

1章 オブジェクト指向設計

現状認識

  • プロダクトが進化・変化し続ける限り、「コードの変更」が必須
  • オブジェクト指向のアプリケーションにおいて変更が困難なのは、オブジェクトがメッセージのやりとりをする限り、メッセージの受け手と送り手が互いのことを知っている必要があるから = 依存関係があるから
    • オブジェクト指向設計とは、その依存関係を上手いこと管理するための設計
    • 方針としては、一つ一つのオブジェクトが極力知識を持ちすぎないようにすること

設計の道具

  1. 設計原則:
    1. SOLID
      • Single Responsibility
      • Open-Closed
      • Liskov Substitution
      • Interface Segregation
      • Dependency Inversion
    2. DRY
    3. Low of Demeter
  2. デザインパターン
    • 頻出の設計における問題とその解決策集

オブジェクト指向設計の簡単な導入

  • オブジェクト指向のアプリは、オブジェクトとメッセージから構成される
    • 実はメッセージのほうが重要
  • オブジェクト指向において、データと振る舞いはともにオブジェクトに収めれる

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

まずはクラス(= オブジェクト)から扱う。メッセージのほうが大事だけど。

変更が簡単なコードとは

  • 定義
    1. 変更は副作用をもたらさない
    2. 要件の変更とコードの変更量は相関する
    3. 既存コードは簡単に再利用できる
    4. 最も簡単な変更方法はコードの追加である
  • 結果、次の特徴を持つ = TRUEなコード
    1. Transparent: 見通しがいい
    2. Reasonable: 合理的
    3. Usable: 利用性が高い
    4. Exemplary: 模範的

単一責任のクラスを作る

  • 2つ以上の責任を持つクラスは簡単には再利用できない
    • 一方の変更や再利用が、他方の責任に副作用をもたらすことがある = クラス内部での絡みつき
  • クラス同士の結びつき = 依存関係があればあるほど、一方の変更が他方のクラスを破壊しうる
    • 多くのことをやりすぎるクラスを持ってはいけない理由
  • クラスのやるべきことを明らかな一言に明示化するべき
    • 例えば下記Gearクラスがタイヤのサイズまで持ってgear_inchesを算出するのは明らかにGearの領分を超えている
# ex1
class Gear
    # attr_readerを付して必ず直接インスタンス変数(= データ)にアクセスさせないようにした方が良い
    # あとで@chainringに対して一定の処理をしたいと思ったときに面倒
    attr_reader :chainring, :cog
    def initialize(chainring, cog)
        @chainring = chainring
        @cog = cog
    end

    def ratio
        chainring / cog.to_f
    end
end
  • データではなく振る舞いに依存するべき
    • = データはなるべく隠蔽したほうが良い
    • データを参照する箇所を一つのメソッドに限定したほうが変更に強い(DRY)
  • データ構造はStructで隠蔽する
    • 配列のどの位置にどういったデータが有るかを知らないと書けないコードは脳内メモリを食う
    • Structを用いてデータ構造をメソッドに置き換える
  • メソッドも単一責任にする
    • 途中計算や、ループ + ループない処理などはできるだけ分離させる

第三章 依存関係を管理する

依存関係を認識する

# ex2

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 # 依存 4点
    end

    def ratio
        chainring / cog.to_f
    end
# ...
end

class Wheel
    attr_reader :rim, :tire
    def initialize(rim, :tire)
        @rim = rim
        @tire = tire
    end

    def diameter
        rim + (tire * 2)
    end
end

puts Gear.new(52, 11, 26, 1.5).gear_inches
  • 次のことをオブジェクトが知っている時、依存関係を持つ = オブジェクト間の結合(CBO: Cuopling Between Objects)
    1. 他のクラスの名前
    2. self以外の何処かに送ろうとするメッセージの名前
      • なるべくメッセージはselfに送るようにする
      • self以外に送るメッセージはいずれどこかで再利用すると考え、DRYのためにあらかじめインスタンスメソッドとしてかき分けておくと良い
      • demeterの法則にも対応できる
    3. メッセージが要求する引数
    4. それら引数の順番
  • 他の依存関係
    • メソッドチェーン ... 途中のクラスに変更があると、チェーン全体に悪影響
    • テスト ... テストとコードの過度な結合はテストのコストを引き上げる
  • 別に引数にオブジェクト渡したって問題ない
  • 下記のように、(インターフェースとなるメッセージである )とあるメソッドを知るDuckだけをもつことで、クラスの切り離しをすることを依存オブジェクトの注入(Dependency Injection)という。
# ここで重要なのは、diameterというメッセージを知る`Duck`を要求すること。
class Gear
    def initialize(chainring, cog, wheel)
        @chainring = chainring
        @cog = cog
        @rim = rim
        @wheel = wheel
    end

    def gear_inches
        # ratio * wheel.diameter
        ratio * diameter
    end

    # messageはなるべくselfに送ると良い
    def diameter
        wheel.diameter
    end
end

Gear.new(52, 11, Wheel.new(26, 1.5))
  • 引数の順番への依存を除去する
    1. 初期化引数にhashを使うと良い
      • ||を利用すればデフォルト値も付与できる
      • args.fetch(:chainring, 40)のようにfetchメソッドでデフォルト値を付与するのもよい
      • defaultsメソッドを用意し、defaults.merge(args)もよい。特にデフォルト値が複雑な場合に。
    2. ruby2.0.0からはキーワード引数もある
    3. 引数の順番をargsに置換できない場合(ex. 外部ライブラリの利用)、moduleを利用して、ライブラリをラップしてあげればよい
      1. クラスの唯一の目的が他クラスのインスタンスの作成であるオブジェクトをファクトリーと呼ぶ
module GearWrapper
    def gear(args)
        SomeLibrary::Gear.new(args[:chainring], args[:rim])
    end
end

依存方向の管理

  • これまでの例: GearがWheelに依存
  • 依存方向の選択 = 変更が少ないものに依存するべき
    1. 変更コミットの少ないクラス
    2. より上位 = 抽象的なクラス

shareシェアする

forumコメント

まだコメントはありません!
ログインしてコメントを残す
{{comment.user.name}} on {{commentCreatedAt()}}

content_copy前後のイシュー

{{message}}