1 前言
当我们使用Spring开发应用时,无需在程序中调用Spring的代码,就可使用Spring的功能特性。比如依赖注入、MVC,从而开发出高内聚低耦合的应用代码。
我们自己也写代码,能够做到让其他工程师不调用我们的代码就可以使用我们的代码的功能特性吗?大多数开发者应该做不到吧!那么Spring是如何做到的?
2 定义
DIP是指一种特定的解耦(传统的依赖关系创建在高层次,而具体的策略设置则应用在低层模块)形式,使得高层模块不依赖于低层模块的实现细节,依赖关系被反转,从而使得低层模块依赖于高层模块的需求抽象。 该原则规定:
- 高层模块不应该依赖低层模块,二者都应该依赖抽象接口
- 抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口
该原则颠倒了一部分人对OOP的认识方式。如高层、低层对象都应该依赖相同的抽象接口。
常规应用的分层架构,策略层会依赖方法层,业务逻辑层会依赖数据存储层。这种高层模块依赖低层模块的分层架构有什么缺点呢?
3 传统分层架构缺陷
3.1 维护困难
高层模块通常是业务逻辑和策略模型,是一个软件的核心。正是高层模块使一个软件区别于其他软件,而低层模块则更多的是技术细节。若高层模块依赖低层,就是业务逻辑依赖技术细节,技术细节改变将影响业务逻辑。由技术细节改变而影响业务代码,这显然不合理。
3.2 复用困难
通常越高层,复用价值越高。但若高层模块依赖低层模块,那么对高层模块的依赖将会导致对底层模块的连带依赖,复用困难。
4 更多案例
在日常开发中,很多地方都使用了依赖倒置原则。比如访问数据库,代码并非直接依赖DB驱动,而是依赖JDBC。各种DB驱动都实现了JDBC,当应用程序需要更换DB时,无需修改任何代码。这正是因为应用代码,高层模块,不依赖DB驱动,而是依赖抽象JDBC,而DB驱动作为低层模块,也依赖JDBC。
Web应用也无需依赖Tomcat容器,只需依赖J2EE规范,Web应用实现J2EE规范的Servlet接口,然后把应用程序打包通过Web容器启动即可处理HTTP请求了。Web容器可以是Tomcat、Jetty,任何实现了J2EE规范的Web容器。 其他MVC框架,ORM框架,也都遵循该原则。
5 设计原理
下面,我们进一步了解下依赖倒置原则的,看看如何在我们的程序设计开发中也能利用依赖倒置原则,开发出更少依赖、更低耦合、更可复用的代码。
习惯上策略层依赖方法层,方法层依赖工具层。该分层依赖的一个潜在问题是,策略层对方法层和工具层是传递依赖,下面两层的任何改动都会导致策略层改动,这种传递依赖导致的级联改动可能会导致软件维护过程非常糟糕。
解决办法是利用依赖反转,每个高层模块都为它所需要的服务声明一个抽象接口,而低层模块则实现这些抽象接口,高层模块通过抽象接口使用低层模块。
这样高层无需直接依赖低层模块,而变成了低层模块依赖高层模块定义的抽象接口,从而实现了依赖反转,解决了传递依赖问题。
所以日常开发通常也都依赖抽象接口,而不是依赖具体实现。 那么Web开发中,Service层依赖DAO层,并非直接依赖DAO的具体实现,而是依赖DAO提供的抽象接口。那么这种依赖是否是依赖反转呢? 并不是,依赖反转原则中,除了具体实现要依赖抽象,最重要的是,抽象是属于谁的抽象。
通常低层模块拥有自己的接口,高层模块依赖低层模块提供的接口,比如方法层有自己的接口,策略层依赖方法层的接口;DAO层定义自己的接口,Service层依赖DAO层定义的接口。
但是按照依赖反转原则,接口的所有权被反转,即接口被高层模块定义,高层模块拥有接口,低层模块实现接口。不是高层模块依赖底层模块的接口,而是低层模块依赖高层模块的接口,从而实现依赖关系反转。
在上面的依赖层次中,每层接口都被高层模块定义,由低层模块实现,高层模块完全不依赖低层模块,即使是低层模块的接口。这样,低层模块的改动不会影响高层模块,高层模块的复用也不会依赖低层模块。对于Service和DAO,就是Service定义接口,DAO实现接口,这样才符合依赖反转。
6 改造案例
依赖反转适于一个类向另一个类发送消息的场景。
6.1 常规的依赖实现设计
Button按钮控制Lamp灯泡,按钮按下,灯泡点亮或关闭。
- 按常规设计,可能会设计如下UML,Button类直接依赖Lamp类。
public class Button {
private Lamp lamp;
public void Poll() {
if (/* 某条件 */) {
lamp.TurnOn();
}
}
}
该设计问题是Button依赖Lamp,对Lamp的任何改动,都可能牵扯Button,还无法重用Button类。比如,我们期望通过Button控制一个电机的启动或者停止,这种设计显然难以重用Button,因为我们的Button还在依赖Lamp。
应该将该设计中的依赖实现,重构为依赖抽象(这里就是打开/关闭目标对象)。
6.2 依赖抽象设计
由Button定义一个抽象接口ButtonServer
,其中描述抽象:打开/关闭目标对象。
由具体的目标对象,比如Lamp实现该接口,从而完成Button控制Lamp。
通过依赖反转,Button不再依赖Lamp,而是依赖抽象ButtonServer,Lamp也依赖ButtonServer,高层模块和低层模块都依赖抽象。Lamp改动不影响Button,而Button也可复用以控制其他实现ButtonServer接口的目标对象。 抽象接口ButtonServer的所有权反转,它不属底层模块Lamp,而属高层模块Button。
依赖反转也就是不要来调用我,我会调用你。Tomcat、Spring都是基于该原则,应用程序不需要调用Tomcat或者Spring这样的框架,而是框架调用应用程序。而实现这一特性的前提就是应用程序必须实现框架的接口规范,比如实现Servlet接口。
7 总结
高层模块不依赖低层模块,都依赖抽象接口,该抽象接口通常由高层定义,低层实现。 可得如下编程实践:
- 多使用抽象接口,避免使用具体的实现类
- 不要继承具体类,若一个类在设计之初不是抽象类,就尽量不继承它。对具体类的继承是一种强依赖,维护时难以改变
- 不要重写包含具体实现的方法
- 框架的设计。框架提供核心功能,比如HTTP处理,MVC等,并提供一组接口规范,应用程序只需要遵循接口规范编程,就可被框架调用。程序使用框架的功能,但不调用框架的代码,而是实现框架的接口,被框架调用,所以框架才有如此高可复用性
参考
- 依赖反转原则
- https://flylib.com/books/en/4.444.1.71/1/