依赖倒置原则(Dependency Inversion Principle)

很多软件工程师都多少在处理 "Bad Design"时有一些痛苦的经历。如果发现这些 "Bad Design" 的始作俑者就是我们自己时,那感觉就更糟糕了。那么,到底是什么让我做出一个能称为 "Bad Design" 的设计呢?

绝大多数软件工程师不会在设计之初就打算设计一个 "Bad Design"。许多软件也在不断地演化中逐渐地降级到了一个点,而从这个点开始,有人开始说这个设计已经腐烂到一定程度了。为什么会发生这些事情呢?是因为最初设计的匮乏吗,还是设计逐步降级到像块腐烂的肉一样?实际上,寻找这些答案得先从确定 "Bad Design" 的准确定义开始。

"Bad Design" 的定义

你可能曾经提出过一个让你倍感自豪的软件设计,然后让你的一个同事来做 Design Review?你能感觉到你同事脸上隐含的抱怨与嘲弄,他会冷笑的问道:"为什么你要这么设计?" 反正这事儿在我身上肯定是发生过,并且我也看到在我身边的很多工程师身上也发生过。确切的说,那些持不同想法的同事是没有采用与你相同的评判标准来断定 "Bad Design"。我见过最常使用的标准是 "TNNTWI-WHDI" ,也就是 "That‘s not the way I would have done it(要是我就不会这么干)" 标准。

但有一些标准是所有工程师都会赞同的。如果软件在满足客户需求的情况下,其呈现出了下述中的一个或多个特点,则就可称其为 "Bad Design":

  1. 难以修改,因为每次修改都影响系统中的多个部分。(僵化性Rigidity)
  2. 当修改时,难以预期系统中哪些地方会被影响。(脆弱性Fragility)
  3. 难以在其他应用中重用,因为它不能从当前系统中解耦。(复用性差Immobility)

此外,还有一些较难断定的 "Bad Design",比如:灵活性(Flexible)、鲁棒性(Robust)、可重用性(Reusable)等方面。我们可以仅使用上面明确的三点作为判定一个设计的好与坏的标准。

"Bad Design" 的根源

那到底是什么让设计变得僵化、脆弱和难以复用呢?答案是模块间的相互依赖

如果一个设计不能很容易被修改,则设计就是僵化的。这种僵化性体现在,如果对相互依赖严重的软件做一处改动,将会导致所有依赖的模块发生级联式的修改。当设计师或代码维护者无法预期这种级联式的修改所产生的影响时,那么这种蔓延的结果也就无法估计了。这导致软件变更的代价无法被准确的预测。而管理人员在面对这种无法预测的情况时,通常是不会对变更进行授权,然后僵化的设计也就得到了官方的保护。

脆弱性是指一处变更将破坏程序中多个位置的功能。而通常新产生的问题所涉及的模块与该变更所涉及的模块在概念上并没有直接的关联关系。这种脆弱性极大地削弱了设计与维护团队对软件的信任度。同时软件使用者和管理人员都不能预测产品的质量,因为对应用程序某一部分简单的修改导致了其他多个位置的错误,而且看起来还是完全无关的位置。而解决这些问题将可能导致更多的问题,使得维护过程陷进了 "狗咬尾巴" 的怪圈。

如果设计中实现需求的部分对一些与该需求无关的部分产生了很强的依赖,则该设计陷入了死板区域。设计师可能会被要求去调查是否能够将该设计应用到不同的应用程序,要能够预知该设计在新的应用中是否可以完好的工作。然而,如果设计的模块间是高度依赖的,而从一个功能模块中隔离另一个功能模块的工作量足以吓到设计师时,设计师就会放弃这种重用,因为隔离重用的代价已经高于重新设计的代价

示例:一个拷贝程序(Copy)

通过一个简单的例子来描述这些问题可能会对我们有所帮助。设想有一个简单的 "Copy" 程序,它负责将键盘上输入的字符拷贝到一个打印机上。假设设备独立而且是与平台无关的。我们可以构思这个程序的结构,类似于图 1 中的描述:

图 1 拷贝程序

图 1 是一个结构图。它显示在应用程序中一共有三个模块,或者叫子程序。"Copy" 模块负责调用其他两个模块。可以简单的想象成在 "Copy" 中有一个 while 循环,在循环体内调用 "Read Keyboard" 模块来尝试从键盘读取一个字符,然后将字符发送到 "Write Printer" 模块来打印字符。

1 void Copy()
2 {
3     int c;
4     while ((c = ReadKeyboard()) != EOF)
5         WritePrinter(c);
6 }

这个两层的模块设计是可以很好地被重用的,它们可以被使用到许多不同的应用程序中来控制对键盘和打印机的访问。

然而,"Copy" 模块在那些不使用键盘和打印机的条件下是无法被重用的。这太可惜了,因为系统所呈现的智能化就是体现在了这个模块里。"Copy" 模块封装了我们所感兴趣并且希望重用的部分。

例如,假设我们有一个新的程序,它需要将键盘字符拷贝到磁盘文件。我们显然希望复用 "Copy" 模块,因为它所做的高层封装正是我们需要的。而这个封装所做的就是描述将字符从源拷贝到目的地的过程。但很不幸,由于 "Copy" 模块直接依赖了 "Write Printer" 模块,所以这种新的需求情况下无法被重用。

当然,我们可以直接修改 "Copy" 模块来增加新的功能。通过增加 "if" 语句来检查一个标志位,判断到底是写到打印机还是写到磁盘,这样就可以分别使用 "Write Printer" 模块或 "Write Disk" 模块。然后,这样做之后我们就又在系统中增加了一个依赖模块。

 1 enum OutputDevice {printer, disk};
 2 void Copy(outputDevice dev)
 3 {
 4     int c;
 5     while ((c = ReadKeyboard()) != EOF)
 6         if (dev == printer)
 7             WritePrinter(c);
 8         else
 9             WriteDisk(c);
10 }

随时时间的推移,越来越多的设备可以支持 "Copy" 功能,"Copy" 模块也将陷入凌乱的 "if/else" 判断中。这显然使应用变得僵化和脆弱。

依赖倒置(Dependency Inversion)

上述问题的主要特征是包含高层逻辑的模块依赖于低层模块的细节,例如 "Copy" 模块依赖于 "Read Keyboard" 模块和 "Write Printer" 模块。如果我们想办法使 "Copy" 模块不依赖于这些细节,则就会很容易地被复用。可以将其用于任何其他负责从输入设备将字符拷贝到输出设备的应用程序。OOD 为我们提供了一种机制,叫做依赖倒置(Dependency Inversion)。

图 2

设想如图 2 中的类图结构。类 "Copy" 包含了一个抽象类 "Reader" 和另一个抽象类 "Writer"。可以想象在 "Copy" 中的循环结构不断的从 "Reader" 读取字符,然后将字符发送至 "Writer"。

 1 class Reader
 2 {
 3     public:
 4         virtual int Read() = 0;
 5 };
 6 class Writer
 7 {
 8     public:
 9         virtual void Write(char) = 0;
10 };
11 void Copy(Reader& r, Writer& w)
12 {
13     int c;
14     while((c=r.Read()) != EOF)
15         w.Write(c);
16 }

此时类 "Copy" 既没有依赖 "Keyboard Reader" 也没有依赖 "Printer Writer"。因此,这些依赖已经被反转了(Inverted)。"Copy" 类依赖于抽象,而真正的 "Reader" 和 "Writer" 的具体实现也依赖于抽象。

此时,我们就可以重用 "Copy" 类,而不需要具体的 "Keyboard Reader" 和 "Printer Writer"。我们可以通过创造新的 "Reader" 和 "Writer" 衍生类然后替换到 "Copy" 中。而且,无论有多少种 "Reader" 和 "Writer" 被创建,"Copy" 都不会依赖于它们。因为没有这些模块间的相互依赖,也使得程序不会变的僵化和脆弱。并且 "Copy" 类也可以被复用到多种不同的情况中。它不再是固定的。

依赖倒置原则(The Dependency Inversion Principle)

A. High level modules should not depend upon low level modules. Both should depend upon abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstraction.

A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

B. 抽象不应该依赖于具体实现细节,而具体实现细节应该依赖于抽象。

有人可能会问,为什么我要使用 "Inversion" 这个词儿。坦白的说,是因为,对于更加传统的软件开发方法,例如结构化的分析与设计(Structured Analysis and Design),更趋向于创建高层模块依赖于低层模块的软件结构,进而使得抽象依赖了具体实现细节。而且实际上这些方法最主要的目标就是通过定义子程序的层级关系来描述高层模块式如何调用低层模块的。图 1 中的示例正好描述了这样的一个层级结构。因此,一个设计良好的面向对象程序的依赖结构是 “inverted” 倒置了相对于传统过程化方法的依赖结构。

考虑下高层模块依赖于低层模块所带来的连带影响。高层模块包含着应用程序中重要的业务决策信息,是这些业务模型包含了应用程序的功能特征。当这些模块依赖于低层模块时,对低层模块的修改将直接影响高层模块,也就是强制修改了它们。

这种情形是违反常理的!应该是高层模块强制要求修改低层模块才对。高层模块的权重应该优先于低层模块。高层模块是无论如何都不应当依赖于低层模块。

更进一步说,我们其实想重用的是高层模块。我们已经通过子程序库等方式很好地重用了低层模块了。如果高层模块依赖于低层模块,将导致高层模块在不同的环境中变得极难被复用。而如果高层模块完全独立于与低层模块,高层模块就可以很容易地被复用。这就是这个原则的核心所在。

分层(Layering)

依据 Grady Booch 的定义:

All well-structured object-oriented architectures have clearly-defined layers, with each layer providing some coherent set of services though a well-defined and controlled interface.

所有结构良好的面向对象架构都有着清晰明确的层级定义,每一层都通过一个定义良好和可控的接口来提供一组内聚的服务集合。

如果不加思索的来解释这段话,可能会让设计师创建出类似于图3中的结构。

图 3

在图中高层类 "Policy" 使用了低层类 "Mechanism","Mechanism" 使用了更细粒度的 "Utility" 类。这看起来像是很合适,但其实隐藏了一个问题,就是对于 Policy Layer 的更改将对一路下降至 Utility Layer。这称为依赖是传递的(Dependency is transitive)。Policy Layer 依赖一些依赖于 Utility Layer 的模块,然后 Policy Layer 传递性的依赖了 Utility Layer。这显示是非常不幸的。

图 4 给出了一个更合适的模型。

图 4

每一个低层都被一个抽象类所表述,而实际的层级则由这些抽象类所派生。每一个高层类通过抽象接口来使用低层类。因此,层级之间不会依赖其他的层。取而代之的是,层依赖了抽象类。这不仅打破了 Policy Layer 到 Utility Layer 的传递性依赖,同时也将 Policy Layer 到 Mechanism Layer 的依赖打破。

使用这个模型后,Policy Layer 不会被任何 Mechanism Layer 或 Utility Layer 的更改所影响。同时,Policy Layer 也能够在任何情形下进行重用,只要是低层模块符合 Mechanism Layer Interface 定义即可。因此,通过反转依赖关系,沃恩稿件了一个更灵活、更持久的设计结构。

总结

依赖倒置原则(Dependency Inversion Principle)是很多面向对象技术的根基。它特别适合应用于构建可复用的软件框架,其对于构建弹性地易于变化的代码也特别重要。并且,因为抽象和细节已经彼此隔离,代码也变得更易维护。

面向对象设计的原则

 SRP
单一职责原则


Single Responsibility Principle

 OCP
开放封闭原则


Open Closed Principle

 LSP
里氏替换原则


Liskov Substitution Principle


ISP


接口分离原则


Interface Segregation Principle

 DIP
依赖倒置原则


Dependency Inversion Principle

参考资料

本文《依赖倒置原则(Dependency Inversion Principle)》由 Dennis Gao 翻译改编自 Robert Martin 的文章《DIP: The Dependency Inversion Principle》,未经作者本人同意禁止任何形式的转载,任何自动或人为的爬虫行为均为耍流氓。

依赖倒置原则(Dependency Inversion Principle)

时间: 2024-10-24 19:27:20

依赖倒置原则(Dependency Inversion Principle)的相关文章

依赖倒置原则(Dependence Inversion Principle,DIP)

依赖倒置原则: A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象. B.抽象不应该依赖于具体,具体应该依赖于抽象. 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成.这种场景下,类A一般是高层模块,负责复杂的业务逻辑:类B和类C是低层模块,负责基本的原子操作:假如修改类A,会给程序带来不必要的风险. 解决方案:将类A修改为依赖接口I,类B和类C各自实现

依赖反转(Dependency inversion principle)和控制反转(Inversion of Control)

有两个对象A和B,A some B  是A依赖于B,当B some A的时候是B依赖于A这就叫依赖反转: 这种依赖关系如果让程序员自己控制(new 对象),就会出现高耦合,控制反转(依赖注入)就是让这种依赖关系由第三方管理(eg:spring)而不是程序员自己管理. 名词解释 依赖:一种模型元素之间的关系的描述.例如类A调用了类B,那么我们说类A依赖于类B. 耦合:一种模型元素之间的关系的描述.例如类A调用了类B或类B调用了类A,那么我们说类A与类B有耦合关系. 耦合度:模型元素之间的依赖程度的

依赖倒置(Dependence Inversion Principle)DIP

using System; using System.Collections.Generic; using System.Text; namespace DependenceInversionPrinciple { //依赖倒置(Dependence Inversion Principle)DIP //High level modules should not depend upon low level modeules.Both should depend upon abstractions.

设计模式六大原则(3):依赖倒置原则

依赖倒置原则 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成.这种场景下,类A一般是高层模块,负责复杂的业务逻辑:类B和类C是低层模块,负责基本的原子操作:假如修改类A,会给程序带来不必要的风险. 解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率. 依赖倒转原则(Dependency

Java 设计模式(十二) 依赖倒置原则(DIP)

依赖倒置原则(Dependence Inversion Principle) 依赖倒置原则(DIP)的基本概念 原始定义 高层模块不应该依赖低层模块,两者都应该依赖其抽象 抽象不应该依赖细节 细节应该依赖抽象 Java中的具体含义 模块间的依赖通过抽象发生 实现类之间不发生直接的依赖关系 其依赖关系通过接口或者抽象类产生 接口或抽象类不依赖于具体实现 实现类依赖接口或抽象类 依赖倒置(DIP)的好处 采用DIP可以减少类之间的耦合性,提高稳定性,降低并行开发带来的风险,提高代码的可读性和可维护性

Dependency Inversion Principle(依赖倒置原则)

本文由@呆代待殆原创,转载请注明出处. Dependency Inversion Principle(依赖倒置原则,之后简称DIP):不要依赖实例(concrete classes)编程,依赖抽象(abstractions,指依赖抽象类和接口). 关于倒置(inversion)的理解:通常我们的高层组件都会依赖于低层组件(指某个低层具体实例类),而DIP是不允许这样的,在DIP的指导下,我们会创建一个抽象类,让它处于高层组件与低层组件之间,打破这种依赖,这时不仅高层组件会依赖于这个抽象类,低层组

设计模式原则(3)--Dependency Inversion Principle(DIP)--依赖倒转原则

1.定义: 高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 抽象不应该依赖于细节,细节应当依赖于抽象.换言之,要针对接口编程,而不是针对实现编程. 2.使用场景: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成.这种场景下,类A一般是高层模块,负责复杂的业务逻辑:类B和类C是低层模块,负责基本的原子操作:假如修改类A,会给程序带来不必要的风险.即将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C

依赖倒转原则(Dependency Inversion Principle,DIP)

依赖于抽象.具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象:抽象不依赖于具体,具体依赖于抽象. 依赖一定会存在于类与类.模块与模块之间.当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标. 抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心.依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在

设计模式六大原则---依赖倒置原则(DIP)

定义 依赖倒置原则(Dependency Inversion Principle) 核心思想:依赖于抽象     具体体现: 体现一:高层模块不应该依赖低层模块.两个都应该依赖抽象. 体现二:抽象不应该依赖细节.细节应该依赖抽象. 依赖倒置原则告诉我们:细节是多变的,而抽象是相对稳定的.所以我们编程的时候要注重抽象的编程,而非细节编程. 实例 1.AGP插槽:主板和显卡之间的关系的抽象.主板和显卡通常是使用AGP插槽来连接的,这样,只要接口适配,不管是主板还是显卡更换,都不是问题. 2.驾照:司