敏捷开发笔记(第11章节)--依赖倒置原则(DIP)

 

目录

 

1:PDF上传链接

11.1 依赖倒置原则(DIP)

11.2 层次化

11.2.1 倒置的接口所有权

11.2.2 依赖于抽象

11.3 一个简单的例子

        找出潜在的抽象

11.4 熔炉示例

动态多态性与静态多态性

11.5 结论


1:PDF上传链接

【免费】敏捷软件开发(原则模式与实践)资源-CSDN文库

11.1 依赖倒置原则(DIP)

        a高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
        b.抽象不应该依赖于细节。细节应该依赖于抽象。

        在这些年中,有许多人曾经问我为什么在这条原则的名字中使用“倒置”这个词。这是由于许多传统的软件开发方法,比如结构化分析和设计,总是倾向于创建一些高层模块依赖于低层模块、策略(pocy)依赖于细节的软件结构。实际上这些方法的目的之一就是要定义子程序层次结构,该层次结构描述了高层模块怎样调用低层模块。图7.1中C0py程序的初始设计就是这种层次结构的一个典型示例。一个设计良好的面向对象的程序,其依赖程序结构相对于传统的过程式方法设计的通常结构而言就是被“倒置”了。
        请考虑一下当高层模块依赖于低层模块时意味着什么。高层模块包含了一个应用程序中的重要的策略选择和业务模型。正是这些高层模块才使得其所在的应用程序区别于其他。然而,如果这些高层模块依赖于低层模块,那么对低层模块的改动就会直接影响到高层模块,从而迫使它们依次做出改动。
        这种情形是非常荒谬的!本应该是高层的策略设置模块去影响低层的细节实现模块的。包含高层业务规则的模块应该优先并独立于包含实现细节的模块。无论如何高层模块都不应该依赖于低层模块。
        此外,我们更希望能够重用的是高层的策略设置模块。我们已经非常擅长于通过子程序库的形式来重用低层模块。如果高层模块依赖于低层模块,那么在不同的上下文中重用高层模块就会变得非常困难。然而,如果高层模块独立于低层模块,那么高层模块就可以非常容易地被重用。该原则是框架(framework)设计的核心原则。

11.2 层次化

        Booch曾经说过:“…所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供了一组内聚的服务。”①对这个陈述的简单理解可能会致使设计者设计出类似图11.1的结构。图中,高层的PolicyLayer使用了低层的Mechanism Layer,而MechanismLayer又使用了更细节的层Utility Layer。这看起来似Policy Layer乎是正确的,然而它存在一个隐伏的错误特征,那Mechanism就是:Policy Layer对于其下一直到Utility Layer的改动Layer都是敏感的。这种依赖关系是传递的。Policy LayerUtility Layer依赖于某些依赖于Utility Layer的层次;因此PolicyLayer传递性地依赖于Utility Layer。这是非常糟糕的。

图11.1简单的层次化方案 

        图11.2展示了一个更为合适的模型。每个较高层次都为它所需要的服务声明一个抽象接口,较低的层次实现了这些抽象接口,每个高层类都通过该抽象接口使用下一层,这样高层就不依赖于低层。低层反而依赖于在高层中声明的抽象服务接口。这不仅解除了PolicyLayer对于UtilityLayer的传递依赖关系,甚至也解除了PolicyLayer对于MechanismLayer的依赖关系。

图11.2 倒置层次 

        请注意这里的倒置不仅仅是依赖关系的倒置,它也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口。但是当应用了DIP时,我们发现往往是客户拥有抽象接口,而它们的服务者则从这些抽象接口派生。

11.2.1 倒置的接口所有权

        这就是著名的Hollywood原则:“Don't call us,we''ll call you.”(不要调用我们,我们会调用你。)低层模块实现了在高层模块中声明并被高层模块调用的接口。
        通过这种倒置的接口所有权,对于MechanismLayer或者UtilityLayer的任何改动都不会再影响到PolicyLayer。而且,PolicyLayer可以在定义了符合PolicyServicelnterface的任何上下文中重用。这样,通过倒置这些依赖关系,我们创建了一个更灵活、更持久、更易改变的结构。

11.2.2 依赖于抽象

        一个稍微简单但仍然非常有效的对于DP的解释,是这样一个简单的启发式规则:“依赖于抽象。”这是一个简单的陈述,该启发式规则建议不应该依赖于具体类一也就是说,程序中所有的依赖关系都应该终止于抽象类或者接口。

根据这个启发式规则,可知:

        任何变量都不应该持有一个指向具体类的指针或者引用。

        任何类都不应该从具体类派生。

        任何方法都不应该覆写它的任何基类中的已经实现了的方法。

        当然,每个程序中都会有违反该启发规则的情况。有时必须要创建具体类的实例,而创建这些实例的模块将会依赖于它们。此外,该启发规则对于那些虽是具体但却稳定(nonvolatile)的类来说似乎不太合理。如果一个具体类不太会改变,并且也不会创建其他类似的派生类,那么依赖于它并不会造成损害。
        比如,在大多数的系统中,描述字符串的类都是具体的。例如,在Java中,表示字符串的是具体类String。该类是稳定的,也就是说,它不太会改变。因此,直接依赖于它不会造成损害。
        然而,我们在应用程序中所编写的大多数具体类都是不稳定的。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在抽象接口的后面,可以隔离它们的不稳定性。
        这不是一个完美的解决方案。常常,如果一个不稳定类的接口必须要变化时,这个变化一定会影响到表示该类的抽象接口。这种变化破坏了由抽象接口维系的隔离性。
        由此可知,该启发规则对问题的考虑有点简单了。另一方面,如果看得更远一点,认为是由客户类来声明它们需要的服务接口,那么仅当客户需要时才会对接口进行改变。这样,改变实现抽象接口的类就不会影响到客户。

11.3 一个简单的例子

        依赖倒置可以应用于任何存在一个类向另一个类发送消息的地方。例如,Button对象和Lamp对象之间的情形。
        Buto对象感知外部环境的变化。当接收到Pol消息时,它会判断是否被用户“按下”。它不关心是通过什么样的机制去感知的。可能是GU亚上的一个按钮图标,也可能是一个能够用手指按下的真正按钮,甚至可能是一个家庭安全系统中的运动检测器。Buto对象可以检测到用户激活或者关闭它。
        Lamp对象会影响外部环境。当接收到TurnOn消息时,它显示某种灯光。当接收到TurnOff消息时,它把灯光熄灭。它可以是计算机控制台的LED,也可以是停车场的水银灯,甚至是激光打印机中的激光。

        该如何设计一个用Button对象控制Lamp对象的系统呢?图11.3展示了一个不成熟的设计。Buto对象接收Poll消息,判断按钮是否被按下,接着简单地发送TurnOn或者TurO开消息给Lamp对象。

图11.3展示了一个不成熟的设计 

        为何说它是不成熟的呢?考虑一下对应这个模型的Java代码(见程序11.1)。请注意Button类
直接依赖于Lamp类。这个依赖关系意味着当Lamp类改变时,Button类会受到影响。此外,想要重
用Button来控制一个Motor对象是不可能的。在这个设计中,Button控制着Lamp对象,并且也只能
控制Lamp对象。

程序11.1 Button.java
public class Button 
{
    private Lamp itsLamp;
    public void poll() 
    {
        if ( /* some condition */ )
        {
            itsLamp.turnon();
        } 
    }
}

        这个方案违反了DIP。应用程序的高层策略没有和低层实现分离。抽象没有和具体细节分离。
没有这种分离,高层策略就自动地依赖于低层模块,抽象就自动地依赖于具体细节。

        找出潜在的抽象

        什么是高层策略呢?它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。它是系统内部的系统一一它是隐喻(metaphore)。在Button/Lamp例子中,背后的抽象是检测用户的开关指令并将指令传给目标对象。用什么机制检测用户的指令呢?无关紧要!目标对象是什么?同样无关紧要!这些都是不会影响到抽象的具体细节。
        通过倒置对Lamp对象的依赖关系,可以改进图11.3中的设计。在图11.4中,可以看到Button现在和一个称为ButtonServer的接口关联起来了。ButtonServer接口提供了-一些抽象方法,Button可以使用这些方法来开启或者关掉一些东西。Lamp实现了ButtonServer接口。这样,Lamp现在是依赖于别的东西了,而不是被依赖了。

图11.4 

        图11.4中的设计可以使Button控制那些愿意实现ButtonServer接口的任何设备。这赋予我们极大的灵活性。同时也意味着Buto对象将能够控制还没有被创造出来的对象。
        不过,这个方案对那些需要被Button控制的对象提出了一个约束。需要被Button控制的对象必须要实现ButtonServer接口。这不太好,因为这些对象可能也要被Switch对象或者一些不同于Button的对象控制。
        通过倒置依赖关系的方向,并使得Lamp依赖于其他类而不是被其他类依赖,我们使Lamp依赖于一个不同的具体细节Button。是这样吗?

        Lamp的确依赖于ButtonServer,但是ButtonServer没有依赖于Button。任何知道如何去操纵ButtonServer接口的对象都能够控制Lamp。因此,这个依赖关系只是名字上的依赖。可以通过给ButtonServer起一个更通用-一点的名字,比如SwitchableDevice,来修正这一点。也可以确保把Button和SwitchableDevice被放置在不同的库中,这样对SwitchableDevice的使用就不必包含对Button的使用。
        在本例中,接口没有所有者。这是一个有趣的情形,其中接口可以被许多不同的客户使用,并被许多不同的服务者实现。这样,接口就需要被放置在一个单独的组(goup)中。在C++中,可以把它放在一个单独的namespace和库中。在Java中,可以把它放在一个单独的package中。

11.4 熔炉示例

        我们来看一个更有趣的例子。考虑一个控制熔炉调节器的软件。该软件可以从一个0通道中读取当前的温度,并通过向另一个IO通道发送命令来指示熔炉的开或者关。算法结构看起来如程序11.2所示。

程序11.2一个温度调节器的简单算法
#define TERMOMETER 0x86
#define FURNACE 0x87
#define UNGAGE 1
#define DISENGAGE 0

void Regulate(double minTemp, double maxTemp)
{
    fo上(;;) {
        while (in(THERMONETER) > maxTemp) 
            wait (1);
        out (FURNACE, ENGAGE);

        while (in(THERMONETER) < minTemp)
            wait (1);
        out (FURNACE, DISENGAGE)
    }
}

        算法的高层意图是清楚的,但是实现代码中却夹杂着许多低层细节。这段代码根本不能重用于不同的控制硬件。

        由于代码很少,所以这样做不会造成太大的损害。但是,即使是这样,使算法失去重用性也是可惜的。我们更愿意倒置这种依赖关系,结果如图11.5所示。
        图中显示了Regulate函数接受了两个接口参数。Thermoeter接口可以读取,而Heater接口可以启动和停止。Regulate算法需要的就是这些。它的实现如程序l1.3所示

图11.5通用的调节器

程序11.3通用的调节器
void Regulate (Thermometer&t, Heater&h, double minTemp, double maxTemp)
{
    for(;;){
        while (t.Read() > maxTemp) 
            wait (1);
        h.Engage ();

        while (t.Read < minTemp)
            wait(1):
        h.Disengage ();
    }
}

        这就倒置了依赖关系,使得高层的调节策略不再依赖于任何温度计或者熔炉的特定细节。该算法具有很好的可重用性。

动态多态性与静态多态性

11.5 结论

        使用传统的过程化程序设计所创建出来的依赖关系结构,策略是依赖于细节的。这是糟糕的,因为这样会使策略受到细节改变的影响。面向对象的程序设计倒置了依赖关系结构,使得细节和策略都依赖于抽象,并且常常是客户拥有服务接口。
        事实上,这种依赖关系的倒置正是好的面向对象设计的标志所在。使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。如果程序的依赖关系不是倒置的,它就是过程化的设计。
        依赖倒置原则是实现许多面向对象技术所宣称的好处的基本低层机制。它的正确应用对于创建可重用的框架来说是必须的。同时它对于构建在变化面前富有弹性的代码也是非常重要的。由于抽象和细节被彼此隔离,所以代码也非常容易维护。

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SOLID原则中的依赖倒置原则(Dependency Inversion Principle,DIP)是指高层模块不应该依赖底层模块,二者都应该依赖于抽象接口;抽象接口不应该依赖于具体实现,而具体实现应该依赖于抽象接口。 简单来说,DIP原则就是通过接口来解耦高层模块和底层模块之间的依赖关系,使得系统更加灵活、可维护、可扩展。在设计和开发过程中,我们应该遵循DIP原则,尽可能使用接口或抽象类来定义模块之间的依赖关系,而不是直接依赖具体实现类。 举个例子,假设我们正在开发一个电商系统。我们有一个OrderService类,它依赖于一个底层模块的OrderDao类来实现订单数据的持久化。如果我们直接在OrderService类中实例化OrderDao对象,那么OrderService类就与OrderDao类紧密耦合,如果我们需要更换一种不同的数据持久化方案,那么就需要修改OrderService类的代码,违反了开闭原则(Open Close Principle,OCP)。 为了遵循DIP原则,我们可以先定义一个抽象的OrderDao接口,然后让OrderService类依赖于OrderDao接口。底层模块的具体实现类可以实现OrderDao接口,这样就可以实现数据持久化的功能,同时也可以轻松地更换不同的数据持久化方案,不需要修改OrderService类的代码。 总之,DIP原则是设计模式中非常重要的原则之一,它可以帮助我们构建更加灵活、可维护、可扩展的系统。在实际开发中,我们应该尽可能地遵循DIP原则,使用接口或抽象类来定义模块之间的依赖关系,降低模块之间的耦合度,提高系统的可维护性和可扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值