2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[メモ]単一責任と依存関係の管理

Posted at

この記事は「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」を読んだまとめです。

オブジェクト指向とは

コードの変更を容易に保つために「依存関係」を管理するテクニック。ソフトウェアは事業の要望などにより変更と追加が発生するが、行き当たりばったりに実装していると依存関係が複雑になり小さな変更でも大きな影響を与えるようになる。依存関係が複雑になると、変更するコストが高くなり、不具合も発生しやすくなるので事業の運営に悪影響を与える。オブジェクト指向は依存関係を適切にコントロールして誰でも変更しやすいソフトウェアを作るための考え方とテクニック。

単一責任のクラスを作る

一つのクラスには一つの責任(目的、用途)とするべきという考え方。おそらくメソッドも一つの責任の方がいい。一つの責任しかなければ、そのクラスに変更が発生した場合、どんな影響があるか把握しやすい。複数の責任があると変更による影響を把握しづらい。

クラスが単一責任になっているか確認するには、一文でクラスを説明してみる。「それと」「または」が含まれているようであれば責務が複数存在する可能性が高い。メソッドも複数の責任があるとメソッド名を決められなかったり長くなりがち。

単一責任は「凝縮度」とも言い換えることができる。クラスの全てがそのクラスの中心的な目的に関連していればそのクラスは凝縮度が高い。

Gearクラスの責任は「歯のある二つのスプロケット間の比を計算する」とすると以下のクラスの例のgear_inchesメソッドはタイヤの大きさまで口をだしているのでやりすぎている。「自転車へのギアの影響を計算する」ことであればgear_inchesメソッドはしっくりくるがtireを知っていることへはピンとこない。Gearクラスは二つ以上の責務を持ってしまっている。

class Gear
  attr_reader:chainring,:cog,:rim,:tire
  definitialize(chainring,cog,rim,tire)
    @chainring=chainring
    @cog=cog
    @rim=rim
    @tire=tire
  end

  def ratio
    chainring/cog.to_f
  end

  def gear_inches
  #タイヤはリムの周りを囲むので、直径を計算するためには2倍する
  ratio*(rim+(tire*2))
  end
end

puts Gear.new(52,11,26,1.5).gear_inches
# >137.090909090909

このクラスをすぐに改善する必要はあるのか?
「今すぐ改善」「あとで改善」の緊張感は常に存在する。コストが最小限になるように、現在の要件と未来の可能性の相互間のトレードオフを理解して決断を下すべき。

依存関係を理解する

一方のオブジェクトに変更を加えた時に、他方のオブジェクトも変更せざるを得ないおそれがあるならば、片方に依存しているオブジェクトがある。自身のアプリケーション内のクラスは、自身のアプリケーションが所有するコードにのみ依存するべき。

以下のコードはGearがWheelオブジェクトに以下の点で依存している。

  • GearはWheelという名前のクラスが存在することを知っている
  • GearはWheelインスタンスがdiameterに応答することを予想している
  • Gearはwheel.newにrimとtireが必要なことを知っている
  • GearはWheel.newの最初の引数がrimで二番目の引数がtireである必要があることを知っている。

class Gear
  attr_reader:chainring,:cog,:rim,:tire
  definitialize(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

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

end

依存オブジェクトの注入

Gearが必要としているのはdiameterを返すオブジェクトであり、Wheelではない。(ダックタイピングのオブジェクト)

GearからWheelへの参照が、gear_inchesメソッド内という深いところでハードコーディングされているとき、それは明示的に「Wheelインスタンスのギアインチしか計算する意思はない」と宣言していることにほかなりません。Gearはそれがどんな種類のものであれ、Wheel以外のオブジェクトとの共同作業を拒絶します。

diameterに応答ができるオブジェクトを渡すことで、以下の依存関係は解消される。

  • GearはWheelという名前のクラスが存在することを知っている
  • Gearはwheel.newにrimとtireが必要なことを知っている
  • GearはWheel.newの最初の引数がrimで二番目の引数がtireである必

このテクニックを 「依存オブジェクトの注入(dependencyinjection)」という。


class Gear
  attr_reader:chainring,:cog,:wheel
  definitialize(chainring,cog,wheel)
    @chainring=chainring
    @cog=cog
    @wheel = wheel
  end

  def gear_inches
    ratio*wheel.diameter
  end

制限が厳しくWhealをGearに注入できない場合はGearクラス内でWheelインスタンスの作成を隔離できないか考える。そうすることでWheelに依存しているものの、依存数は減り、依存が明確になる。

initializeメソッドでwheelインスタンスを作成する


class Gear
  attr_reader:chainring,:cog,:rim,:tire
  definitialize(chainring,cog,rim,tire)
    @chainring=chainring
    @cog=cog
    @wheel = Wheel.new(rim,tire)
  end

  def gear_inches
    ratio*wheel.diameter
  end
end

wheelメソッドに隔離する


class Gear
  attr_reader:chainring,:cog,:rim,:tire
  definitialize(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

外部依存の隔離

外部へのメッセージ(メソッドの呼び出し)に注目する。外部メッセージとはself以外に送られるメッセージのこと。


class Gear
  def gear_inches
    ratio*wheel.diameter
  end
end

ratioとwheelはselfへのメッセージだが、diameterはWheelへのメッセージとなっている。
以下のようにしてgear_inchesから依存を取り除くことができる。


class Gear
  def gear_inches
    ratio*diameter
  end

  def diameter 
    wheel.diameter
  end
end

こうすることでWheelがdiameterの名前など変更した場合でもGearへの副作用はシンプルなラッパーメソッド(diameter)に収まるようになる。

外部への参照が埋め込まれていて、参照先が変わる可能性が高い時に参照を隔離することで変更が容易になる。

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

Gearのinitializeは引数の順番に依存している。


class Gear
  attr_reader:chainring,:cog,:wheel
  def initialize(chainring,cog,wheel)
    @chainring=chainring
    @cog=cog
    @wheel = wheel
  end

argsハッッシュを受け取るように変更することで引数の順番への依存から解放される。


class Gear
  attr_reader:chainring,:cog,:wheel
  def initialize(args)
    @chainring=args[:chainring]
    @cog=args[:cog]
    @wheel = args[:wheel]
  end

argがハッシュを受け取ることで、ハッシュキーへの依存が生まれてしまったが、引数の順番の依存と比べると軽微で、かつキーがあることで引数の役割が明確になった。

外部からしようされることが前提のフレームワークやオプションをいくつも受け取るクラスではこのテクニックを使うメリットが大きい。

もしもGearが外部のフレームワークで初期化メソッドは固定の順番を求め場合はGearを初期化するためのラッパーを作成することで依存を回避できる。この用法をファクトリーとも言う。


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

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?