Dependency Management Principle

通过精心设计的对象都具有单一的职责,因此他们实际上是通过合作来完成复杂的任务。这种合作强大而危险。为了实现一个合作,一个对象必须知道其他对象的某些情况。这种知道便创建了一种依赖关系。当两三个对象耦合在一起,它们变回表现得像一个整体,不可能只重用其中的一个。如果对依赖关系不仔细加以管理,那么这些依赖关系将毁掉整个应用程序。

理解依赖关系

  1. 知道另外一个类的名字

    GearWheel 的引用深入到 gear_inches 方法里,并将其硬编码,那么这便是明确说明它只愿意为 Wheel.gear 的实例计算齿轮英寸数,从而拒绝与其他任何类型的对象合作。

  2. 消息的名字

    Gear 需要访问可以相应 diameter 的对象。实际上,对 Gear 来说,计算 gear_inches 并不需要知道这个对象的类,只要这个对象可以相应 diameter 就行了。我们可以把这个对象理解成鸭子类型。

  3. 消息所要求的参数

    Gear 也并不需要知道 Wheel 需要使用 rimtire 进行初始化

  4. 参数的顺序

    Gear 不需要知道 Wheel 初始化时参数的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

def circumference
diameter * Math::PI
end
end

隔离依赖关系

隔离实例的创建

虽然此时 Gear 类仍然知道得太多,但是已经有所改进。如下的编码风格减少了 gear_inches 的依赖关系数量,同时公开暴露了 GearWheel 的依赖。它们没有将依赖关系隐藏起来,而是将它们显露出来。这样降低了重用这段代码的门槛。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.diameter
end


def wheel ## <--- 隔离实例的创建
@wheel ||= Wheel.new(rim,tire)
end
end

隔离外部消息

如下的编码方式,wheel.diameter被深度嵌套在 gear_inches 方法里。这个方法依赖 Gear 才能相应 Wheel,并且依赖Wheel才能响应diameter。完全没有必要在gear_inches里嵌入这种外部依赖,这使得 Gear 更加脆弱。

1
2
3
4
5
def gear_inches
# .... 假设存在额外的数学运算
ratio * wheel.diameter
# .... 假设还存在额外的数学运算
end

现在我们将wheel.gear隔离在一个单独的方法里,并且 gear_inches可以依赖于某条发送给自己的消息。如果 Wheel 更改了diameter的名字或者签名,那么对Gear的副作用将会限定在一个简单的包裹方法里。

1
2
3
4
5
6
7
8
9
def gear_inches
# .... 假设存在额外的数学运算
ratio * diameter
# .... 假设还存在额外的数学运算
end

def diameter
wheel.diameter
end

移除参数顺序依赖关系

  1. 使用散列表来进行初始化同时显示地指定默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Gear
    attr_reader :chainring, :cog, :wheel
    def initialize(args)
    # args = defaults.merge(args)
    @chainring = args.fetch(:chainring,40)
    @cog = args.fetch(:cog,18)
    @wheel = args[:wheel]
    end

    # def defaults
    # {:chainring => 40, :cog => 18}

    # .......
    end
  2. 隔离多参数初始化操作
    有时候我们会被迫依赖某个要求参数初始化顺序的固定方法,由于那里不属于我们的地盘,我们无法更改这个方法。此时可以创建一个单一的方法,将外部接口包裹起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46


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

    def ratio
    chainring / cog.to_f
    end

    def gear_inches
    ratio * wheel.diameter
    end
    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


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

    p GearWrapper.gear(:chainring => 52, :cog => 11, :wheel => Wheel.new(16,1.5)).gear_inches
  3. 反转依赖关系

  • 有些类比其他类更容易管理
  • 具体类比抽象类更容易发生变化
  • 更改拥有许多关系的类会造成广泛的变化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class Gear
    attr_reader :chainring, :cog
    def initialize(chainring,cog)
    @chainring = chainring
    @cog = cog
    end

    def ratio
    chainring / cog.to_f
    end

    def gear_inches(diameter)
    ratio * diameter
    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


    p Wheel.new(16,1.5, 52,11).gear_inches
Donate article here