依赖倒置原则(DIP)
一.定义
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
更加精简的定义就是“面向接口编程”
二.依赖倒置原则的好处
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定,降低并行开发引起的风险,提高代码的可读性和可维护性。
三.依赖倒置原则,究竟倒置在哪里
在依赖倒置原则中的倒置指的是和一般OO设计的思考方式完全相反。
举个例子,现在你需要实现一个比萨店,你第一件想到的事情是什么?我想到的是一个比萨店,里面有很多具体的比萨,如:芝士比萨、素食比萨、海鲜比萨……
比萨店是上层模块,比萨是下层模块,如果把比萨店和它依赖的对象画成一张图,看起来是这样:
没错!先从顶端开始,然后往下到具体类,但是,正如你看到的你不想让比萨店理会这些具体类,要不然比萨店将全都依赖这些具体类。现在“倒置”你的想法……别从上层模块比萨店开始思考,而是从下层模块比萨开始,然后想想看能抽象化些什么。你可能会想到,芝士比萨、素食比萨、海鲜比萨都是比萨,所以它们应该共享一个Pizza接口。对了,你想要抽象化一个Pizza。好,现在回头重新思考如何设计比萨店。
图一的依赖箭头都是从上往下的,图二的箭头出现了从下往上,依赖关系确实“倒置”了
另外,此例子也很好的解释了“上层模块不应该依赖底层模块,它们都应该依赖于抽象。”,在最开始的设计中,高层模块PizzaStroe直接依赖低层模块(各种具体的Pizaa),调整设计后,高层模块和低层模块都依赖于抽象(Pizza)。
四.代码示例
下面是小明同学阅读文学经典的一个类图:
文学经典的源代码:
//文学经典类
public class LiteraryClassic{
//阅读文学经典
public void read(){
System.out.println("文学经典阅读,滋润自己的内心心灵");
}
}
小明类:
//小明类
public class XiaoMing{
//阅读文学经典
public void read(LiteraryClassic literaryClassic){
literaryClassic.read();
}
}
场景类:
public class Client{
public static void main(Strings[] args){
XiaoMing xiaoming = new XiaoMing();
LiteraryClassic literaryClassic = new LiteraryClassic();
//小明阅读文学经典
xiaoming.read(literaryClassic);
}
}
看,我们的实现,小明同学可以阅读文学经典了。
小明同学看了一段文学经典后,忽然他想看看看小说来放松一下自己,我们实现一个小说类:
小说类源代码
//小说类
public class Novel{
//阅读小说
public void read(){
System.out.println("阅读小说,放松自己");
}
}
现在我们再来看代码,发现XiaoMing类的read方法只与文学经典LiteraryClassic类是强依赖,紧耦合关系,小明同学竟然阅读不了小说类。这与现实明显的是不符合的,代码设计的是有问题的。那么问题在那里呢?
我们看小明类,此类是一个高层模块,并且是一个细节实现类,此类依赖的是一个文学经典LiteraryClassic类,而文学经典LiteraryClassic类也是一个细节实现类。这是不是就与我们说的依赖倒置原则相违背呢?依赖倒置原则是说我们的高层模块,实现类,细节类都应该是依赖与抽象,依赖与接口和抽象类。
为了解决小明同学阅读小说的问题,我们根据依赖倒置原则先抽象一个阅读者接口,下面是完整的uml类图:
IReader接口:
public interface IReader{
//阅读
public void read(IRead read){
read.read();
}
}
再定义一个被阅读的接口IRead:
public interface IRead{
//被阅读
public void read();
}
再定义文学经典类和小说类:
文学经典类:
//文学经典类
public class LiteraryClassic implements IRead{
//阅读文学经典
public void read(){
System.out.println("文学经典阅读,滋润自己的内心心灵");
}
}
小说类:
//小说类
public class Novel implements IRead{
//阅读小说
public void read(){
System.out.println("阅读小说,放松自己");
}
}
再实现小明类:
//小明类
public class XiaoMing implements IReader{
//阅读
public void read(IRead read){
read.read();
}
}
然后,我们再让小明分别阅读文学经典和小说:
Client:
public class Client{
public static void main(Strings[] args){
XiaoMing xiaoming = new XiaoMing();
IRead literaryClassic = new LiteraryClassic();
//小明阅读文学经典
xiaoming.read(literaryClassic);
IRead novel = new Novel();
//小明阅读小说
xiaoming.read(novel);
}
}
至此,小明同学是可以阅读文学经典,又可以阅读小说了,目的达到了。
为什么依赖抽象的接口可以适应变化的需求?这就要从接口的本质来说,接口就是把一些公司的方法和属性声明,然后具体的业务逻辑是可以在实现接口的具体类中实现的。所以我们当依赖对象是接口时,就可以适应所有的实现此接口的具体类变化。
五.依赖的三种方法
依赖是可以传递,A对象依赖B对象,B又依赖C,C又依赖D,……,依赖不止。只要做到抽象依赖,即使是多层的依赖传递也无所谓惧。
对象的依赖关系有三种方式来传递:
构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入:
构造函数注入:
//小明类
public class XiaoMing implements IReader{
private IRead read;
//构造函数注入
public XiaoMing(IRead read){
this.read = read;
}
//阅读
public void read(){
read.read();
}
}
Setter方法传递依赖对象
在类中通过Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入:
//小明类
public class XiaoMing implements IReader{
private IRead read;
//Setter依赖注入
public setRead(IRead read){
this.read = read;
}
//阅读
public void read(){
read.read();
}
}
接口声明依赖
在接口的方法中声明依赖对象,在为什么我们要符合依赖倒置原则的例子中,我们采用了接口声明依赖的方式,该方法也叫做接口注入。