LoginSignup
0
0

More than 5 years have passed since last update.

オブジェクト指向設計実践ガイド 3章 依存関係を管理する 簡易まとめ

Posted at

この記事の目的

オブジェクト指向設計実践ガイドを読んで学んだことを簡単にまとめる。復習したい時に全ページを読み返すのは大変なので。
初心者なので間違っている部分があるかもしれません。指摘していだだけると嬉しいです。
(この記事は、オブジェクト指向とは何かを説明するものではなく、単にオブジェクト指向設計実践ガイドの各章を簡易的にまとめたものです。)

この章の目的

振る舞いが他のオブジェクトに定義されている時に、それにアクセスすることについて考える。適切に設計されたオブジェクトは単一責任であるため、問題の解決にはオブジェクト同士が共同作業をしなくてはならない。共同作業をするためには他のオブジェクトに関する知識が必要。しかし、この「知識」は依存関係を作るため、慎重に管理しなくてはならない。

依存関係を理解する

依存関係とはどのようなものか理解するために、以下のコードを用いる。

# このコードはオブジェクト指向設計実践ガイドからの引用です。

class Gear
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire= tire
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * Wheel.new(rim, tire).diameter
  end
end

class Wheel
  attr_reader :rim, :tire

  def initialize(rim, tire)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire*2)
  end
end

Gear.new(52, 11, 26, 1.5).gear_inches

一方のオブジェクトを変更した時、他方のオブジェクトを変更せざるを得ない場合、片方に依存しているオブジェクトがある。

  • このコードは以下のような依存を持っている
    • 他のクラスの名前を知っている
    • self以外にメソッドを送っている
    • 他のクラスのメソッドが必要とする引数を知っている(Wheel#initialize)
      • それらの引数の順番を知っている(Wheel#initialize(rim ,tire))

これらの依存によって、Wheelへの変更によってGearが変更される可能性が高くなる。これら2つのオブジェクトが共同作業をするためにはある程度の依存が必要だが、現在のコードでは不必要な依存が多いため、それらをなくして最低限の依存で動くようにしなくてはならない。

疎結合なコードを書く

依存を減らすとは、必要のない依存を認識して取り除くこと。
以下では依存を減らすためのコーディングテクニックについて説明する。

依存オブジェクトの注入

ほかのクラスにクラス名で参照すると依存が生まれる。
以下のコードでWheelクラスの名前が変更された時、Gear#gear_inchesも変更する必要がある。

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
end
# ~~~~~~
Gear.new(52, 11, 26, 1.5).gear_inches

GearとWheelが共同作業をするためにはどこかで何かがWheelのインスタンスを作成しなければならないため、「クラス名で参照する」という依存は無害に思えるが、このコードは見つけにくい問題を持っている。
現在のコードはWheelへの参照がgear_inches内にハードコーディングされているため、Gearは「Wheel以外のギアインチを計算しない」と宣言しているようなものである。Wheel以外のdiameter(直径)を持ち、ギアを扱うオブジェクトがあったとしても、Gearクラスはそのオブジェクトのギアインチを計算できない。

GearがWheel以外のオブジェクトのギアインチを計算できるようになるためには、コードを変更しなくてはならない。Gearがギアインチを計算するためにはdiameterに応答できるオブジェクト(ダックタイプ)が必要。Gearはギアインチを計算したいオブジェクトがdiameterを知っていればよいので、そのオブジェクトのクラス名や初期化法を知る必要はない。
以下のコードではGearの初期化時にdiameterに応答するオブジェクトを渡す。

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
end
# ~~~~~~
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

Gearはdiameterに応答できるオブジェクトを@wheelというインスタンス変数に入れているが、Gearはこの変数がWheelクラスのインスタンスかどうかを知らない。Gearが気にすることはwheel.diameterが可能かどうかだけ。

Wheelインスタンスの作成をGearの外で行うようにしたことで、2つのクラスの間の結合が切り離された。これでGearはdiameterを持つどんなオブジェクトとも共同作業できるようになった。
このテクニックを「依存オブジェクトの注入」と呼ぶ。依存オブジェクトの注入ができるかどうかは「クラス名を知る責任」や「そのクラスに送るメソッド名を知る責任」が別の場所に属するのではないかと考えることで判別する。

では、「dimeterに応答するオブジェクトを知る責任」はどこにあるのか?

依存を隔離する

不要な依存はクラス内で隔離しておいて、状況が許すようになった時に簡単に特定して除去できるようにする。

インスタンス(変数?)の作成を分離する()

何かの制約によりGearにWheelを注入できない時は、Gear内でWheelインスタンスの作成を隔離する。
以下に2つの例を示す
1つ目はWheelインスタンスの作成をGear#gear_inchesからGear#initializeに移動する。

class Gear
  attr_reader :chainring, :cog, :rim, :tire, :wheel

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end

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

2つ目はWheelインスタンスの作成を新しく定義されたGear#wheelで行うようにする。

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.diameter
  end

  def wheel
    @wheel ||= Wheel.new(rim, tire)
  end
  # ~~~~~~
end

これらの変更では、Gearはまだ依存(Wheelの初期化方法の知識、Wheelインスタンスの作成を内部で行う)を持っているが、Gear#gear_inchesの依存数が減り、GearのWheelへの依存が明らかになった。このようなコードを書くことで依存が明確になり、再利用が簡単になる(コードのリファクタリングが簡単に)。

外部メッセージを隔離する

外部メッセージとは、self以外に送られるメッセージのこと。

def gear_inches
  ratio * wheel.diameter
end

上記のコードではratio, wheelはselfに送られるが、diameterはwheelに送られる。
このコードはシンプルだが、状況がもっと複雑になることもある。

def gear_inches
  # 難しい計算
  foo = hogehoge * wheel.diameter
  # もっと難しい計算
end

このコードでは、wheel.diameterは複雑なメソッドの深いところに埋め込まれてしまっている。このメソッドはGear#wheelwheel.diameterが呼び出し可能であることに依存している。この外部への依存により、何かが変更されるたびにこのメソッドが壊れる可能性がある。
gear_inchesへの変更の可能性を減らすためには、外部への依存を取り除き、専用のメソッドにカプセル化する。

def gear_inches
  ratio * diameter
end

def diameter
  wheel.diameter
end

gear_inchesは外部クラス(Wheelクラス)と「その外部クラスのメソッド」に依存していたが、変更後はselfに送るメソッドに依存するようになった。これによって、gear_inches以外でwheel.diameterを参照する際もself.diameterを使うことができるので、コードがDRYになる。もしWheelがdiameterに変更を加えてもGear内の変更はGear#diameterだけで済む。
このテクニックはメッセージへの参照がクラス内に埋め込まれていて、そのメッセージが変わる可能性が高い時に必要になる。参照を隔離することで、変更が起きた時の影響を少なくできる。

引数の順番への依存を取り除く

引数を要求するメッセージを送る時、送り手は引数についての知識を持たざるを得ない。また、送り手が引数を渡す時は、その引数を渡す順番についても認識している必要がある。
以下のコードではGear#initializeは3つの引数を要求し、初期化するためには正しいものを正しい順番で渡す必要がある。

class Gear
  attr_reader :chainring, :cog, :wheel

  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end
end

Gear.new(52, 11, Wheel.new(26, 1.5))

この引数の順番への依存を、メソッドの引数をinitialize(args={})とすることで取り除くことができる(キーワード引数でもできる)。キーの名前が引数に関するドキュメントにもなる。

class Hoge
  def initialize(args={})
    @foo = args[:foo]
    @bar = args[:bar]
    @baz = args[:baz]
  end
end

Hoge.new(foo: 1, bar: '2', baz: 3)

外部フレームワークの引数への依存

「引数の順番への依存を取り除く」で取り上げたコードは、メソッドを自分で変更できる場合には有効だが、固定順の引数を必要とするものには外部フレームワークのメソッドなど、自分で変更を加えることができないものもある。
以下はGearクラスが外部フレームワークによって定義されていて、変更を加えることのできないという状況のコードである。

module SomeFramework
  class Gear
    attr_reader :chainring, :cog, :wheel

    def initialize(chainring, cog, wheel)
      @chainring = chainring
      @cog = cog
      @wheel = wheel
    end
    # ~~~~
  end
end

class Wheel
  # ~~~
end

module GearWrapper
  def self.gear(args)
    SomeFramework::Gear.new(
      args[:chainring],
      args[:cog],
      args[:wheel]
    )
  end
end

GearWrapper.gear(
  :chainring => 52,
  :cog => 11,
  :wheel => Wheel.new(26, 1.5)
).gear_inches

このような時は、GearWrapperモジュールを用いて、Gearのインスタンス作成を包み隠すことで依存を1箇所に留めることができる。もしアプリケーション内で頻繁にGearのインスタンスを作成しなければならない場合、GearWrapperがあれば「固定順の引数を渡す」コードを繰り返し書かずに済むため、DRYなコードになる。
また、GearWrapper.gearは引数にハッシュを要求するので、SomeFramework::Gearのインスタンスを作成するものに分かりやすいインターフェイスを提供できる。
GearWrapperの唯一の責任は他のクラスのインスタンスを作成することだが、このようなオブジェクトのことを「ファクトリー」と呼ぶ

依存方向の管理

依存関係には方向があるが、その方向はどのように決めれば良いか説明する。

依存関係の逆転

これまでの例ではGearWheel または Gear#diameterに依存していたが、この依存関係を逆転して、WheelGear や Gear#gear_inchesに依存するコードを書くこともできる。

class Gear
  attr_reader :chainring, :cog

  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 initialize(rim, tire, chainring, cog)
    @rim = rim
    @tire = tire
    @gear = Gear.new(chainring, cog)
  end

  def diameter
    rim + (tire * 2)
  end

  def gear_inches
    gear.gear_inches(diameter)
  end
end
Wheel.new(26, 1.5, 52, 11).gear_inches # 137.0909090909091

このコードを見ると、依存の方向が変わったからといって、明らかな変化はないように見える。2つのオブジェクトは今も共同作業をしていて、計算の結果も同じである。
依存の方向が重要になるのは、変更が要求される未来である。将来もたらされる変更に適切に対応できるようになるため、依存方向は適切に選択しなくてはならない。

依存方向の選択

クラス同士が依存しなくてはならない場合は、「自分より変更されないものに依存する」ということを意識する。

  • このフレーズは以下の考え方がもとになっている
    • あるクラスは他のクラスより要件が変わりやすい
    • 具象クラスは抽象クラスより変わる可能性が高い
    • 多くの依存を持ったクラスを変更すると、影響が広範囲に及ぶ

あるクラスは他のクラスより要件が変わりやすい

依存する先のコードがどれくらい安定しているのかを意識しなくてはならない。自分で書いていない外部のフレームワークも変わる可能性がある。

具象クラスは抽象クラスより変わる可能性が高い

[依存オブジェクトの注入]で見たように、依存オブジェクトの注入をする前は具象的なコードに依存していたが、注入によってGearはdiameterメソッドに応答できるダックタイプに依存するようになった。これでGearは以前より抽象的なものに依存するようになった。
Wheelに依存していた時は、GearはWheelへの変更による影響を大きく受けていたが抽象的なダックタイプに依存するようになったことで安定性が増した。

多くの依存を持ったクラスを変更すると、影響が広範囲に及ぶ

多くの依存を持ったクラスへの変更が大きな影響をもたらすことは明白だが、多くの依存を持ったクラスを「持つ」ことの自体の影響も考慮する必要がある。多くの依存を持ったクラスへの変更がアプリケーションの様々なところに影響を与えることが分かっているため、そのクラスを「変更しないこと」が常に求められる。変更が生み出す影響を避けるために、そのクラスに手を加えなくなることは、アプリケーションのハンデの1つとなってしまう。

問題となる依存関係を発見する

上記で述べた事柄がアプリケーションのコードのどこかに当てはまるとすると、アプリケーションを構成するものを次の図に分類できる。

依存されている数
A : 抽象領域(変更少、影響大) D : 危険領域(変更多、影響大)
B : 中立領域(変更大、影響少) C : 中立領域(変更多、影響少)
要件が変わる可能性

アプリケーションの全てのクラスをこの表に分類すると、適切に設計されているアプリケーションのクラスはA,B,Cに集まる。

  • それぞれの領域の特徴
    • A : 抽象クラスやインターフェイスが含まれることが多い
    • B : 依存されておらず、また変更も少ないもの
    • C : 具象的で変わりやすいクラスが含まれる
    • D : 頻繁に変更され、また多く依存されている具象的なもの

アプリケーションの設計をする時は上記の表を参照しながら、「自分より変更されないものに依存する」を意識してコードを書く。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0