依赖倒置原则(The Dependency-Inversion Principle)的本质是要破除看似自然的结构化程序设计思维:逻辑高层调用底层,逻辑上的高层在设计中应处于抽象层次的底层。
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
为什么要叫Inversion(倒置)?这是因为在面向过程编程的年代,逻辑高层调用底层是很正常的设计,因为底层相对稳定。但这没有考虑逻辑高层的复用性。有了面向对象设计后,通过抽象可以让逻辑高层不调用底层,反而是底层去实现高层定义的接口。从UML看,貌似依赖关系倒置过来了,但要注意,这并不意味着底层依赖逻辑高层了,他们两个都依赖抽象层。
通过增加一个接口层,让逻辑高层和底层不再存在依赖关系。逻辑高层定义底层需要实现的接口,然后在自己的逻辑中调用这些接口,它不关心底层对这些接口的实现细节。底层实现这些接口,当底层发生变化时,只要保持接口层不变,逻辑高层不会受影响。而且也没有增加底层对逻辑高层的依赖。
还有一点要注意的是,这里不光去掉的依赖关系,还有接口所有权的倒置。下面会提到这个的好处。上层提出接口需求,拥有这些接口,下层实现这些接口的功能,供上层调用。
做到这些后,逻辑高层模块就可以很方便的复用了。This principle is at the very heart of framework design.
Naive layering scheme
Inverted Layers
DIP告诉我们要depend on abstraction, should not depend on a concrete class.
No variable should hold a pointer or reference to a concrete class.
No class should derive from a concrete class
No method should override an implemented method of any of its base classes.
上述规则很严,但是否依赖concrete class,关键在于class是否稳定。比如Java中String,很稳定,产生依赖也没关系。即使采用上述规则,当接口发生改变时,接口的调用者不可避免需要同时作出改变。但如果接口是client声明的呢,那只有当client需要时,接口才需要发生变化。接口实现类的变化不会影响client。这就是接口所有权的倒置。
This is the reason that the heuristic is a bit naïve. If, on the other hand, we take the longer view that the client classed declare the service interfaces that they need, then the only time the interface will changes is when the client needs the change. Changes to the classes that implement the abstract interface will not affect the client.
看看书中的开关灯的例子,按照我们以前的设计逻辑,Button直接调用Lamp的开关方法很合理。但作者马上指出,这个开关可以用来控制马达吗?显然要修改Button的代码。
Naïve Model of a Button and Lamp
通过增加一个SwitchableDevice接口,Button提出这个接口需求,灯和马达都可以实现这个接口,这样Button就可以在不修改代码的情况下控制灯和马达了。
Dependency Inversion Applied to the Lamp
重点也是难点在于找出潜在的抽象。抽象是什么,抽象就是当细节发生变化,它不发生变化的东西,抽象往往隐藏在系统中。Button/Lamp的例子中,抽象是开关这个动作。至于谁在使用这个开关,这个开关去控制谁,都是细节。
多态是实现DIP的必要条件,client提出接口需求,不同的server实现接口。有了多态,client就不需要理会使用哪个server,不需要理会不同的server的实现细节。
多态分为动态多态和静态多态。我们常说的是动态多态,C++中的模板则属于静态多态。通过静态多态也可以实现DIP,但使用静态多态时,server代码的改变会导致client重编译,所以如果不是对性能有很苛刻的需求,使用动态多态。
是否做到了DIP,是区分面向对象和面向过程的主要标志。DIP是面向对象技术诸多好处的基础,是框架复用的必要条件。对于代码能够尽可能范围内使用变化也是至关重要。由于接口和实现分离,代码也更容易维护。
The principle of dependency inversion is the fundamental low-level mechanism behind many of the benefit claimed for object-oriented technology. Its proper application is necessary for the creation of reusable frameworks. It is also critically important for the construction of code that is resilient to change. Since the abstraction and details are isolated from each other, the code is much easier to maintain.
附Button/Lamp代码实现:
Button.java
public class Button { Switch mSwitch; public Button (Lamp l){ mSwitch = l; } public void poll(boolean isOn){ if (isOn) mSwitch.turnOn(); else mSwitch.turnOff(); } }
Switch.java
public interface Switch { public void turnOn(); public void turnOff(); }
Lamp.java
public class Lamp implements Switch { private boolean mStatus; public Lamp(){ mStatus = false; } @Override public void turnOn() { mStatus = true; } @Override public void turnOff() { mStatus = false; } public boolean getStatus(){ return mStatus; } }