依赖倒置原则
今天 我们来说一下,软件设计中另外一个原则,依赖倒置原则( DIP)
DIP 核心内容
Dependence Inversion Principle 缩写 DIP
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
这个原则的核心内容如下:
-
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
-
抽象不应该依赖细节,细节应该依赖抽象。
看到这个内容的描述 可能会比较疑惑?
高层模块 就是要调用 底层模块啊, 业务类模块(高层模块) 就需要使用工具类的模块啊。 所以为啥 高层模块不能依赖底层模块呢?
请思考一下 如果高层模块 如果依赖 底层模块 意味着什么?
高层模块 我们认为 包含了一些 策略选择或者业务模型。正是由于这些高层的模块才使得是所在的应用程序区别与其他。 然后如果高层模块依赖 低层模块,如果底层模块一但发生了改动,对于高层模块影响是非常大的。
所以对于这种情况 我们应该尽量 使用 高层模块 独立于 底层模块,两者独立开来。
倒置接口的所有权
"don’t call us , we’ll call you " 不要调用我们,我们会调用你。
即 底层模块 实了高层 模块中声明并被高层模块中调用的接口。
依赖抽象
DIP 原则 一个简单的解释 “依赖于抽象” 就是不应该依赖于 具体的类,也就是说 程序中所有的依赖关系都应该是抽象类 或者接口 。
DIP 还可以简单理解成要依赖于抽象,由此,还可以推导出一些指导编码的规则:
- 任何变量都不应该指向一个具体类;
- 任何类都不应继承自具体类;
- 任何方法都不应该覆写父类中已经实现的方法。
这里 当然 有的时候我们的程序并不是那么的严格,有违法的情况。 有时候就要创建一个具体的类,而创建实例的模块就会 依赖它们。 如果具体的类,是非常稳定的,几乎不太可能发生变化,也没有多大的问题。
在Java 中 String
类,Python 内置的类 list
, dict
, tuple
,str
这些本身在Python中就不能修改. 因为这些类是稳定的,直接使用也没有啥问题。
举一个例子
Button
和 Lamp
按钮 可以控制 灯的开关, 而灯 有自己的打开,关闭方法。
如果在 Button
直接使用 lamp
的实现类
如下面的代码
class Lamp:
"""
电灯
可以认为是 底层模块
"""
def turn_on(self):
print("灯泡亮了")
def turn_off(self):
print("灯泡灭了")
class Button:
"""
button 可以认为是 高层模块
"""
def __init__(self, lamp: Lamp):
self.lamp = lamp
def poll(self):
if 'some condition':
self.lamp.turn_on()
else:
self.lamp.turn_off()
这里的代码 Button
依赖于 Lamp
. 这样的 依赖关系 就会有问题, 当 Lamp
发生改动的时候 ,直接会影响到 Button
类, 并且此时 Button
只能控制 Lamp
对象。
解决方案
我们要想办法 找出一个抽象层 ,使 Button 不太依赖 Lamp .
计算机科学中的所有问题都可以通过引入一个间接层得到解决。
All problems in computer science can be solved by another level of indirection
—— David Wheeler
此时我们需要找到一个中间层, 来解决这个问题。
如下图所示:
来构建抽象层,首先思考 对应Lamp 这层的进行抽象,它就是需要 打开 和关闭 这两个方法。
class IDevices(metaclass=ABCMeta):
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
然后Lamp
来实现这个接口
class Lamp(IDevices):
"""
台灯
"""
def turn_on(self):
print("台灯亮了")
def turn_off(self):
print("台灯灭了")
我甚至还可以有其他类型的灯
class Light(IDevices):
"""
电灯
"""
def turn_on(self):
print("灯泡亮了")
def turn_off(self):
print("灯泡灭了")
对于开关 也实现抽象层的方法。然后 把 抽象层对象传入,当成自己的依赖,注意此时 Switch
依赖的是 一个抽象的 device
不是一个具体的实现类,即此时Switch
依赖于一个抽象,而不是细节。
class Switch(IDevices):
"""
开关
"""
def __init__(self, device: IDevices):
self.device = device
def turn_on(self):
print("打开开关...")
self.device.turn_on()
def turn_off(self):
print("关闭开关...")
self.device.turn_off()
# 测试程序
def main():
# switch = Switch(Light())
switch = Switch(Lamp())
switch.turn_on()
switch.turn_off()
这个例子 看起来比较简单,此时,理解 我们高层模块依赖底层模块的时候,需要依赖一个抽象,而不要依赖一个具体的实现。 这样就你可以 有效的 进行隔离开,高层 和 底层模块。 高层不在依赖底层模块。 底层模块无论如何改动都不会影响,高层模块。 高层模块依赖的是抽象,底层模块 则是具体不同的细节实现。
总结
DIP 帮助我们解决的问题 是 我们应该尽可能依赖抽象,这样的好处是 如果被依赖的对象发生变化的时候,我们几乎不太需要做出什么代码的改变。 如果我们依赖具体的对象,实例。那么一旦 底层模块发生了变化,我们可能就会改变很多高层的代码。软件设计原则基本上也写的差不多了,DIP 原则 可能更多的时候 比较适用于 写框架的代码,一般 业务层 也会用到 但不会特别多。从正常情况下,底层模块 应该是 相对稳定的代码,或者说几乎不太会改动的代码,如果底层的代码需要经常变动,就需要考虑 是否应该放在底层模块, 把经常改动的代码,应该尽可能 往 高层模块里面写,而不要写在底层模块里面。
参考文档
24 | 依赖倒置原则:高层代码和底层代码,到底谁该依赖谁?
敏捷软件开发:原则,模式,实践