頑張るときはいつも今

自称エンジニアのゴリラです。日々精進。

【Ruby】オブジェクト思考設計実践ガイドを読み始めた(第3章)

前回に引き続き、 オブジェクト指向設計実践ガイドの読書記録です。

第3章 依存関係を整理する

先にまとめると

  • 依存関係はなるべく少なくする

    • 必要な場合は、メソッドに閉じ込めて、変更への影響範囲を最小限にする
  • 依存する方向は、変更が少なそうな物に対して

    • 要件が明確
    • 抽象クラス、インタフェース
  • 変更が多いかつ依存されている数が多いクラスは存在させないようにする

依存関係って何?

そもそも依存関係って何でしょうか?

オブジェクト指向 依存関係」と言う、ど素人感が半端ない、ワードで検索してみたところ

こちらのQiita記事を発見しました。

それが下のような物。(改行を含む場合にmdで引用を書く方法分からん。)

プログラムの要素が依存する物といえば、多くの場合は以下のような物を指す事が多いと思います。

  • 参照している他の構成要素(引数や他のクラスなど)
  • 使用しているライブラリやフレームワーク

f:id:wa_football_1120:20200405102210j:plain

前回のGearクラスの例だと、Gearクラスからオブジェクトを生成するためには、

Wheelクラスから生成されたオブジェクトが必要でした。

これが依存関係になります。

Gearの機能を全て使うためには、Wheelが必要になると言うことになります。

他にも実際のプログラムは多くのものに依存しています。

依存関係を作ることは、アプリケーションを作成する上で、必ず必要なものですが、

慎重に管理しないとかなり可読性、拡張性の低い物になってしまいます。

依存関係を理解する

前項で、依存関係を自分で調べた内容を記載していますが、

書籍にはこのように書かれていました。

一方のオブジェクトに変更を加えた時、他方のオブジェクトも変更せざるを得ないおそれがあるのであれば、片方に依存しているオブジェクトがあります。

書籍の通りに下のようなプログラムを考えてみましょう。

class Gear
    
    attr_reader :chainring, :cog, :data

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

    def ratio
        chainring / cog.to_f
    end

    def gear_inches
        ratio * wheel.diameter
    end

end

class Wheel
    attr_reader :rim :tire

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

    def diameter
        rim + (tire * 2)
    end

    def circumference
        diameter * Math::PI
    end
end

@wheel = Wheel.new(10, 1.5)

@gear  = Gear.new(52, 11, @diameter)

このコードにおける依存関係は下の4つです。

  • GearクラスはWheelと言う名前のクラスが存在することを知っている(前提に作られている)
  • GearはWheelのインスタンスdiameterに応答することを知っている
  • GearはWheel.newにrimとtireが必要であることを知っている
  • さらにWheel.newに必要な引数の順番についても知っている

先ほどの画像の通り、GearはWheelに依存しています。

Wheelが変更されたら、Gearも変更しなくてはならないかもしれないコードです。

依存関係自体は別にと言うものではありません。

不必要な依存関係を取り除いていくことが、今回のテーマになります。

オブジェクト間の結合(CBO)

先ほどの例だと、GearがWheelのクラス・メソッドの構造を知れば知るほど、

両者の結合度は高いコードとなります。

Wheelの機能を少し変えたいだけなのに、Gearも変えないといけない状況が発生してくると言うことです。

単体テストの視点で言うと、Gearの機能をテストするだけなのに、同時にWheelの機能のテストにもなっている状況です。

(Whellの機能を変えた場合、Gear, Whellのテストコード, Gearのテストコードを変更しないといけないと言うことになりますね。)

また、Railsなんかでは離れた関係のオブジェクトをメソッドチェーンでつなげていくことが

あると思います。

メソッドチェーンは明らかな依存でありかつ、チェーンの途中経路のオブジェクトどれに変更があっても、

影響が及ぶ可能性があるので、かなり危険なものとなります。

疎結合なコード

先ほどまで書いた内容を避けるための方法としては、

疎結合なコードを書くことです。(いや、急に言われても無理じゃね?と言う意見は置いといて)

依存関係を減らすためのテクニックは存在するんです。

依存オブジェクトを注入

先ほどのGearクラスが下のような場合だったと考えます。

class Gear
    
    attr_reader :chainring, :cog, :data

    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_inchesメソッド内で、Wheelに関する処理がハードコーディングされています。

こういったハードコーディングは、GearとWheelがガチガチに結合している物になります。

後から、別なオブジェクトのdiameterを経由してgear_inchesを求める必要がある場合、

拡張は不可能になってしまいます。

diameterに反応できるオブジェクトを挿入することで、

ダックタイプで反応できるようにします。

class Gear
    
    attr_reader :chainring, :cog, :data

    def initialize(chainring, cog, diameterable_object)
        @chainring     = chainring
        @cog           = cog
        @diameterable  = diameter_object
    end


    def gear_inches
        ratio * diameterable.diameter
    end
end

# 外部からはdiameterメソッドを実装したオブジェクトを挿入する
gear = Gear.new(10, 0.4, Wheel.new(10, 20))
puts gear.gear_inches

diameterを知っているオブジェクト生成を外部に切り出すことで、

結合を切り離すことができます。

これは依存オブジェクトの挿入というテクニックになります。

依存を隔離する

依存関係を取り除くことは技術的には可能かも知れませんが、

現実的に考えるとかなり難しい物になります。(特にRails使う場合なんかは、エンハンスで改善していくケースが多くなりますよね)

そういった場合は、完璧を目指さず、改善を目的とします。

依存している部分をクラス内のメソッドに閉じ込めてしまうことで、

依存している部分を最小限にすることができます。

先ほどのコードとほぼ同様ですが、下のコードの場合は考えます。

class Gear
    
    attr_reader :chainring, :cog, :data

    def initialize(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, :data

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


    def gear_inches
        ratio * wheel.diameter
    end

    private
    def wheel
        @whell ||= Wheel.new(@rim, @tire)
    end

end

特に変化がないように見えますが、Wheelに依存していることが、

少しわかりやすくなったように見えます。

次は、外部メッセージを隔離してみましょう。

外部メッセージとは、self以外に送られるメッセージを示します。

gear_inchesメソッドにおいて、wheelの直径を出していますが、

こいつをメソッドに切り出してあげます。

def gear_inches
    ratio * diameter
end
private
def wheel
    @whell ||= Wheel.new(@rim, @tire)
end

def diameter
    wheel.diameter
end

初っ端からこういった実装が必要になるわけではないのですが、

クラスの実装をメソッドに閉じ込めていますので、依存するメソッドも切り出してあげたほうがいいと言う感じになります。

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

こういったメソッドて結構書きますよね。

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

外部から呼び出される時に、引数の数と順番を知っている状態で呼び出すみたいなコードです。

メソッドで引数が追加されたり、削除された場合に、呼び出しもとを全て変更する必要が出てきます。

引数の受け取り方をHashに変えることでこういったコードを改善することができます。

class Gear
    attr_reader :chainring, :cog, :wheel

    def initialize(args)
        @chainring = args[:chainring]
        @cog       = args[:cog]
        @wheel     = args[:wheel]
    end
end

gear = Gear.new(
    chainring: 52,
    cog: 11,
    wheel: Wheel.new(26, 11)
)

コードの冗長性は増していますが、

引数の順番(index)よりも、キー名に依存させることで変更しやすいコードになりました。

依存方向の管理

先ほどコードで言う、GearがWheelに依存すべきか、

WheelがGearに依存すべきかを考えていく項目です。

依存方向を選択する場合には、変更されない物に依存するほうがいいです。

変更されない物に依存すると言う言葉には、下の意味を包含しています。

  • あるクラスは、他のクラスよりも要件がわかりやすい
    • 要件が分かりやすいのであれば、そこまで変更される可能性は低そうですよね
  • 具象クラスよりも抽象クラス
    • 具象クラスは、独自メソッドも持っているので変更される可能性が高いです
  • 多くの物に依存されたクラスを変更すると、アプリ全体に影響が生じる
    • これは、大量に依存されたクラスはなるべく避けることが望ましいと言うことになります。

依存関係の分類

クラスを要件の変わりやすさクラスに依存している物の数の2軸で考えます。

f:id:wa_football_1120:20200405102309j:plain

各領域について整理すると、

  • 抽象領域
    • 大量のクラスに依存されているクラスはここに落ち着く
    • 例えば抽象クラスとかインタフェース
  • 中立領域#1
    • 設計時にほとんど考慮する必要はない
  • 中立領域2
    • コード(要件)がとても変わりやすい
    • ただ依存されている数は非常に少ないので、影響範囲は少ない
  • 危険領域
    • 変更のそれぞれが全てのクラスに影響する
    • メンテすら難しい状態になる

通常は、抽象領域中立領域に落ち着くべきです。

危険領域に位置するクラスが存在した場合、アプリケーションのメンテはかなり

難しい物になります。

所感

概念的な物が多いので、かなり難しいです。

少しづつ業務とか個人開発に適用できるようにしていきます。