参考书籍:设计模式之禅--秦小波
上篇回顾:上期讲到了里氏替换原则(LSP),讲的不好还请见谅。上篇文末留下了一个问题:“正方形是长方形吗?”。这是一个很经典的LSP问题,我们知道,从几何学角度来看,正方形是特殊的长方形,特殊在长和宽长度相等。从这个角度看,长方形的范围更宽泛:既可以长宽不等,又可以长宽相等,可以认为是包含和被包含的关系。那么在编程思想里,可以认为正方形是长方形吗?我们看下如下长方形的代码:
class Rectangle{
private int length;
private int width;
public void setLength(int length)
{
this.length=length;
}
public int getLength()
{
return this.length;
}
......
}
这个类只有两个简单的属性:长和宽。下面我们再看下正方形的代码:
class Square{
private int length;
private int width;
public void setLength(int length)
{
this.length=length;
this.width=length;//这里要敲下黑板
}
public int getLength()
{
return this.length;
}
......
}
我们发现,由于正方形有长宽相等的限制,所以当我们设置了正方形的长时,宽也需要对应着改变。因而正方形无法完全替代长方形,替换后改变了原有的长方形的性质,因此 从这方面来看,“正方形不是长方形”。那怎么办?从小到大我都觉得他们有联系呀!我们可以这样看待他们的关系:他们都是四边形,却达不到替换的程度。
依赖倒置原则:
定义:依赖倒置原则(DIP),全称Dependence Inversion Principle,这是什么意思呢?先看下原始定义:High level modules should not depend upon low level modules.Both should depend uponabstractions.Abstractions should not depend upon details.Details should depend upon abstractions.简单来说,即是:具体类之间的关系应该依靠接口实现,但接口的设计不能依据具体的某个类。换言之也就是:面向接口编程!
介绍了该原则大概是啥,下面我们具体分析下依赖倒置原则中“依赖”和“倒置”是什么。
什么是依赖?当我们需要用到一个对象时,就要把这个对象引进来,我中有你,就产生了依赖。常用的依赖方式有:
- set的方式赋值进来,然后使用这个对象。
- 构造方法引入对象,然后使用这个对象。
- 接口声明,具体类实现,然后使用这个对象。
那么,倒置该作何解呢。既然有倒置,就应该有“正置”,以我们吃饭为例,按我们人正常的行事逻辑,当我们需要吃饭,第一步是找到食物(即吃什么),然后拿起筷子(用什么餐具),最后开始食用。这个过程中,我们明确的知道,要吃什么,要用什么餐具,一切都是按照所需去取(引入依赖)所得,这就是“正置”。慢慢地我们发现,食物有很多种,并不是只有香蕉,还有鱼肉,面条,青菜等,用的餐具也不再确定,可能用筷子,用勺子,用叉子...怎么办?作为“正置”流程的开发者,代码的流程和逻辑我都知道,所以,我可以根据需求的改变,去修改代码。
大家有没有发现问题呢?如果这个项目并非我一个人完成的,而是多个人完成的,甚至是使用的第三方SDK,我并不能完全知道业务的完整逻辑,那怎么办呢?这个时候“倒置”就表现出了他的优势,我们先定义三个抽象接口:
interface IFood{//食物接口
String name;
...
}
interface ITableware{//餐具接口
String name;
...
}
interface IEat{//用餐接口
public void eat(IFood food,ITableware tableware);
...
}
张三负责拓展食物方向的业务,李四负责餐具方面的拓展,王五负责食用方面的逻辑,在IEat的实现类里,IFood可以是任意子类的实现,如 IFood food=new Rice();//Rice实现了IFood接口,IFood food=new Noddle();//Noddle实现了IFood接口。餐具也是相同的逻辑。再也不必担心食物或者餐具的改变去修改代码了,张三李四王五都可以各自完成自己业务,极大减少了各自的牵绊。
在上面的例子里,Rice,Noddle等具体类不再是被主动调用使用,而是先定义了一个接口,具体的实现类根据需要被引入的。也就是产生依赖的Eat类并不知道需要引入哪些具体依赖类,这些最终被依赖的具体类是通过接口的方式引入的,这就是倒置。
建议:依赖倒置的本质就是通过接口或者抽象类使各个模块尽量独立,实现松耦合,降低彼此之间的影响。秦小波给出了如下几点建议:
- 每个类尽量有接口或者抽象类,或者抽象类接口兼备。接口是依赖倒置的基础。
- 变量的表面类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。一层两层可能看不出来问题,但是如果继承了很多层,我相信你会回头反思为什么当初没有设计成接口或者抽象类的。
- 尽量不要覆写基类的方法,这样会影响基类的本质,改变基类的本来意愿。
- 结合里氏替换原则使用。我们知道里氏替换原则就是讲的父子类关系的,和这里的面向接口编程,有水乳交融之处。
最后,每篇一问:
我们一直说依赖倒置,抽象不能依据具体,这样的说法会在哪些例子里被反驳呢?欢迎留言。