高层模块不应该依赖于低层模块。二者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。
(开闭原则、里氏代换原则和依赖倒转原则的三个实例很相似,原因是它之间的关系很紧密,在实现很多重构时通常需要同时使用这三个原则。开闭原则是目标,氏代换原则是基础,依赖倒转原则是手段。它们相辅相成,相互补充,目标一致,只是分析题时的角度不同而已)
实验一
依赖倒置可以应用于任何存在一个类向另一个类发送消息的地方。例如,Button对象和Lamp对象之间的情形。Button对象感知外部环境的变化。当接收到Poll消息时,它会判断是否被用户“按下”。它不关心是通过什么样的机制去感知的。可能是GUI上的一个按钮图标,也可能是一个能够用手指按下的真正按钮,甚至可能是一个家庭安全系统中的运动检测器。Button对象可以检测到用户激活或者关闭它。
Lamp对象会影响外部环境。当接收到TurnOn消息时,它显示某种灯光。当接收到TurnOff消息时,它把灯光熄灭。具体的物理机制并不重要。它可以是计算机控制台的LED,也可以是停车场的水银灯,甚至是激光打印机中的激光。
该如何设计一个用Button对象控制Lamp对象的系统呢?下图给出了一个不成熟的设计。Button对象接收Poll消息,判断按钮是否被按下,接着简单地发送TurnOn或者TurnOff消息给Lamp对象。
为何说它是不成熟的呢?Button类直接依赖于Lamp类。这个依赖关系意味着当
Lamp类改变时,Button类会受到影响。此外,想要重用Button来控制一个Motor对象是不可能的。在这个设计中,Button控制着Lamp对象,并且也只能控制Lamp对象。
这个方案违反了DIP。应用程序的高层策略没有和低层分离。抽象没有和具体细节分离。没有这种分离,高层策略就自动地依赖于低层模块,抽象就自动地依赖于具体细节。
请根据依赖倒置原则对上面的设计进行重构。
解析(参考):
通过倒置对 Lamp 对象的依赖关系,可以改进为一下设计:
可以看到 Button 现在和一个称为 ButtonServer 的几口关联起来。 ButtonServer 接口提供了一些抽象方, Button 可以使用这些方法来开启或者关掉一些东西。 Lamp 实现了 ButtonServer 接口。这样 Lamp 现在依赖于 ButtonServer,而不是被 Button 所依赖。
上面的设计可以让 Button 控制哪些愿意实现 ButtonServer 接口的任何设备。
实验二
某软件公司开发人员在开发 CRM 系统时发现该系统经常需要将存储在 TXT 或 Excel 文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类 CustomerDAO 中将调用数据格式转换类的方法来实现格式转换。初始设计方案结构如下图所示。
在编码实现上图所示的结构时,该软件公司开发人员发现该设计方案存在一个非常严重的问题。由于每次转换数据时数据来源不一定相同,因此需要经常更换数据转换类。例如:有时候需要将 TXTDataConvertor 改为 ExcelDataConvertor 。此时需要修改 CustomerDAO 的源代码,而且在引入并使用新的数据转换类时也不得不修改 CustomerDAO 的源代码。系统扩展性较差,违反了开闭原则。请使用依赖倒置原则对其进行重构。
解析(参考):
在本实验中,由于 CustomerDAO 针对具体数据转换器类编程,因此在增加新的数据转换器或者更换数据转换器时都不得不修改 CustomerDAO 的源代码。可以通过引入抽象数据转换类解决该问题。此时 CustomerDAO 将针对抽象数据转换类编程,程而可以将具体数据转换类类名存储在配置文件中,,需要更改时无需修改源代码,只需要修改配置文件即可。