北风设计模式课程---依赖倒置原则(Dependency Inversion Principle)

一、总结

一句话总结:

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

1、软件设计中的"Bad Design" ?

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

2、那到底是什么让设计变得僵化、脆弱和难以复用呢?

模块间的相互依赖

3、依赖倒置原则实例?

copy功能依赖键盘输入和打字机输出-->copy依赖reader抽象层输入,writer抽象层输出,键盘输入依赖reader抽象层,打字机依赖于writer抽象层。

二、依赖倒置原则(Dependency Inversion Principle)

转自或参考:依赖倒置原则(Dependency Inversion Principle)
https://www.cnblogs.com/gaochundong/p/dependency_inversion_principle.html">依赖倒置原则(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" 模块来打印字符。

 void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}

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

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

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

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

 enum OutputDevice {printer, disk};
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}

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

依赖倒置(Dependency Inversion)

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

图 2

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

 class Reader
{
public:
virtual int Read() = ;
};
class Writer
{
public:
virtual void Write(char) = ;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}

此时类 "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)是很多面向对象技术的根基。它特别适合应用于构建可复用的软件框架,其对于构建弹性地易于变化的代码也特别重要。并且,因为抽象和细节已经彼此隔离,代码也变得更易维护。

面向对象设计的原则

参考资料

北风设计模式课程---依赖倒置原则(Dependency Inversion Principle)的更多相关文章

  1. 设计模式六大原则(三):依赖倒置原则(Dependence Inversion Principle)

    依赖倒置原则(DIP)定义: 高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 问题由来: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码 ...

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

    依赖倒转原则就是 A.要依赖于抽象,不要依赖于实现.(Abstractions should not depend upon details. Details should depend upon a ...

  3. 北风设计模式课程---最少知识原则(Least Knowledge Principle)

    北风设计模式课程---最少知识原则(Least Knowledge Principle) 一.总结 一句话总结: 最少知识原则(Least Knowledge Principle),或者称迪米特法则( ...

  4. 北风设计模式课程---接口分离原则(Interface Segregation Principle)

    北风设计模式课程---接口分离原则(Interface Segregation Principle) 一.总结 一句话总结: 接口分离原则描述为 "客户类不应被强迫依赖那些它们不需要的接口& ...

  5. 北风设计模式课程---里氏替换原则(Liskov Substitution Principle)

    北风设计模式课程---里氏替换原则(Liskov Substitution Principle) 一.总结 一句话总结: 当衍生类能够完全替代它们的基类时:(Liskov Substitution P ...

  6. 北风设计模式课程---单一职责原则(Single Responsibility Principle)

    北风设计模式课程---单一职责原则(Single Responsibility Principle) 一.总结 一句话总结: 一个类应该有且只有一个变化的原因:单一职责原则(SRP:Single Re ...

  7. 北风设计模式课程---开放封闭原则(Open Closed Principle)

    北风设计模式课程---开放封闭原则(Open Closed Principle) 一.总结 一句话总结: 抽象是开放封闭原则的关键. 1."所有的成员变量都应该设置为私有(Private)& ...

  8. 依赖倒置(Dependence Inversion Principle)DIP

    关于抽象类和接口的区别,可以参考之前的文章~http://www.cnblogs.com/leestar54/p/4593173.html using System; using System.Col ...

  9. ASP.NET 设计模式中依赖倒置原则

    依赖倒置原则 A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象. B.抽象不应该依赖于具体,具体应该依赖于抽象. 依赖倒置原则 A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于 ...

随机推荐

  1. PostgreSQL索引思考

    当在看Monetdb列存行只支持IMPRINTS和ORDERED这两种索引,且只支持定长数值类型时,就在思考,对于列存,还有必要建索引吗?在PostgreSQL的索引就要灵活很多,我对常用列建合理的索 ...

  2. PHP_OS的常见值

    PHP_OS是PHP中的一个预定义常量,表示当前操作系统.那么PHP_OS有哪些值可用呢??PHP_OS的值一般可以为:CYGWIN_NT-5.1,Darwin,FreeBSD,HP-UX,IRIX6 ...

  3. 怎样用adb抓取log?

    在Android客户端的测试过程中,有时候我们会遇到闪退等异常情况.这时我们可以通过adb抓取log,从而给开发提供更多信息. 一.下载ADB.exe     在网上搜索“adb工具包”就可以找到很多 ...

  4. Delphi7所使用的WinAPI大全(摘自VCL源码,一共1200个函数)

    经过我整理的,去掉了A和W的重复.虽然没写注释,但以后要一个一个研究.有这些WINAPI就够用了. kernel32 = 'kernel32.dll'; gdi32 = 'gdi32.dll'; us ...

  5. spark数据结构之RDD

    学习spark,RDD是一个逃不过去的话题,那么接下来我们看看RDD 1.什么是RDD? RDD叫做弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变.可分区.里面元素可以并行计算的集合 ...

  6. 1rem,1em,1vh,1px含义

    rem:相对于页面根元素<html>元素,通常做法是给html元素设置一个字体大小,然后其他元素的大小就是相对于根元素的大小 em:相对于父元素字体大小,元素的width/height/p ...

  7. 前端开发HTML&css入门——伪类选择器和一些特殊的选择器

    伪类和伪元素 有时候,你需要选择本身没有标签,但是仍然易于识别的网页部位,比如段落首行或鼠标滑过的连接.CSS为他们提供一些选择器:伪类和伪元素. 常用的一些伪类选择器: :link :visited ...

  8. Nginx优化_数据包头部信息过大问题

    如果客户端发出请求的URL头部信息过大,网站将不能及时响应,并通过状态码414报错. <center><h1>414 Request-URI Too Large</h1& ...

  9. Ubuntu18 给terminal改个漂亮的命令行提示符

    重新安装了VMware和Ubuntu,但是命令行提示符太单调,不美观,如何更改呢.于是在网上巴拉巴拉搜寻一番. 1.更改PS1环境变量,这俩都可以,我选择第一个: export PS1="\ ...

  10. java实现一个简单的计数器

    package com.fengunion.sf; import org.junit.platform.commons.util.StringUtils; import java.util.HashM ...