DIP 依赖倒置原则
- 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
- 抽象不应该依赖于细节。细节应该依赖于抽象。
依赖于低层模块的高层模块意味着什么?正是高层模块包含了应用程序中重要的策略选择和业务模型。这些高层模块使得其所在的应用程序区别于其他。然而,如果这些高层模块依赖于低层模块,那么对于低层模块的改动会直接影响到高层模块,从而迫使它们依次做出改动。如果高层模块独立于低层模块,那么高层模块就可以非常容易地被重用。该原则是框架设计的核心原则。
层次化
糟糕的层次关系。
更为适合的模型。每个较高层都为它所需要的服务声明一个抽象接口。较低的层次实现了这些抽象接口。每个高层类都通过该抽象接口使用下一层。这样高层就不依赖于低层。低层反而依赖与在高层中声明的抽象服务接口。这不仅解除了PolicyLayer对于UtilityLayer的传递依赖关系,甚至也解除了PolicyLayer对于MechanismLayer的依赖关系。
倒置的接口所有权
倒置不仅仅是依赖关系的倒置,它也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口。但是当应用了DIP时,我们发现往往是客户拥有抽象接口,而它们的服务者则从这些抽象接口派生。
这就是著名的Hollywood原则:"Don‘t call us, we‘ll call you.(不要调用我们,我们会调用你。)"低层模块实现了在高层模块中声明并被高层模块调用的接口。
通过这种倒置的接口所有权,对于MechanismLayer和UtilityLayer的任何改动都不会再影响到PoliyLayer。而且,PolicyLayer可以在定义了符合PolicyServiceInterface的任何上下文中重用。这样,通过倒置这些依赖关系,我们创建了一个更灵活、更持久、更易改变的结构。
这里所说的所有权仅仅是指接口是随拥有它们的客户程序发布的,而非实现它们的服务器程序。接口和客户程序位于同一个包或者库中。这就迫使服务器程序库或者包依赖于客户程序库或者包。
当然,有时我们会不想让服务器程序依赖于客户程序,特别是当有多分客户程序但是服务器却仅有一份时。在这种情况下,客户程序必须得遵循服务接口,并把它发布到一个独立的包中。
依赖于抽象
程序中所有的依赖关系都应该终止于抽象类或者接口。
- 任何变量都不应该持有一个指向具体类的引用。
- 任何类都不应该从具体类派生。
- 任何方法都不应该重写它的任何基类中已经实现了的方法。
当然,每个程序都会有违反该启发规则的情况。有时必须创建具体类的实例,而创建这些实例的模块将会依赖于它们。此外,该启发规则对于那些虽然是具体但却稳定的来来说似乎不大合理。如果一个具体类不太会改变,并且也不会创建其他类似的派生类,那么依赖于它并不会造成损害。
比如,在大多数系统中,描述字符串的类都是具体的。例如,在C#中的String。该类是稳定的。也就是说,它不太会改变。因此,直接依赖于它不会造成伤害。
如果一个不稳定的类的接口必须变化是,这个变化一定会影响到表示该类的抽象接口。这种变化破坏了由抽象接口维系的隔离性。
由此可知,该启发规则对问题的考虑有点儿简单了。另一方面,如果看得更远一点,认为是客户模块或者层来声明它们需要的服务接口,那么仅当客户需要时才会对接口进行改变。这样,改变实现抽象接口的类就不会影响到客户。
找出潜在的抽象
接口可以被许多不同的客户使用,并被许多不同的服务者实现。这样,接口就需要独立存在而不属于任何一方。在C#中,可以把它放在一个单独的命名控件和库中。
结论
使用传统的过程化程序设计所创建出来的依赖关系结构、策略是依赖于细节的。这是糟糕的,因为这样会使策略受到细节的改变的影响。面向对象的程序设计倒置了依赖关系结构,使得细节和策略都依赖于抽象,并且常常是客户程序拥有服务接口。
事实上,这种依赖关系倒置正是好的面向对象设计的标志所在。使用何种语言编程是无关紧要的。如程序的依赖关系是倒置的,它就是面向对象的设计。如果程序的依赖关系不是倒置的,它就是过程化的设计。
依赖倒置原则是实现许多面向对象技术所宣称的好处的基本低层机制。它的正确应用对于创建可重用的框架来说是必需的。同时它对于构建在变化面前富有弹性的代码也是非常重要的。由于抽象和细节彼此隔离,所以代码也非常容易维护。
摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、115-121、