在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由 Robert Martin 在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
依赖反转原则,英文为:Dependency Inversion Principle,简称 DIP
。
面对这个原则,或许你心中有这些疑问:
- 什么是依赖反转呢?反转了谁?
- 依赖反转,经常和控制反转、依赖注入一起被提到,那么控制反转是什么?依赖注入是什么?
- 如果你对Java比较熟悉,那么你肯定听过Spring,Spring自称是控制反转(IOC)框架,Spring和IOC又有什么关系呢?
等等...
每次面对这些问题的时候,心中总是很懵,我们一起来看看这些问题。
控制反转(Inversion Of Control, IOC)
我们先看一下下面的例子:
public class SendMsgDemo {
public static boolean canSendMsg {...}
public static void main(String[] args) {
if(canSendMsg()) {
// 发送消息处理
} else {
// 消息发送失败处理
}
}
}
上面的代码,实现了发送消息的功能,你可以发现,能否发送成功的是由方法 canSendMsg 控制,所以所有流程由程序员控制
,那么我们可以改变一下,由框架去控制流程
呢?
// 将消息发送器抽象
public abstract class MsgSender {
public abstract boolean canSendMsg();
public void doSend() {
if(canSendMsg()) {
// 发送消息处理
} else {
// 消息发送失败处理
}
}
}
// 实现一个消息发送器
public class UserMsgSender extends MsgSender {
public boolean canSendMsg() {...}
}
public class SendMsgDemo {
private static List<MsgSender> senders = new ArrayList<>();
// 注册消息发送器
public static void register(MsgSender sender) {
if(sender != null) {
senders.add(sender);
}
}
public static void main(String[] args) {
// 注册流程,可以通过第三方配置类或者配置文件进行注册 MsgSender
// 为了完整逻辑,当前在这里注册
register(new UserMsgSender());
// 执行消息发送逻辑
for (MsgSender sender : senders) {
sender.doSend();
}
}
}
在上面的例子中,我们在框架中预留了扩展点 MsgSender , 也就是,我们实现发送信息的逻辑,只需要自定义 MsgSender 即可实现功能,无需再次修改框架逻辑 main,这是典型利用模板设计模式实现控制反转的例子。
框架提供的是代码骨架,通过扩展点组装对象并控制执行流程。程序员在编写框架时,需要思考预留好扩展点,通过扩展点定制化自己的业务,自定义的业务逻辑借用框架,链接驱动整个程序流程的执行。
“控制”是指对程序执行流程的控制;“反转”是由程序员控制程序执行流程,到框架控制程序执行流程,控制权从程序员转移到框架上。
控制反转并非实现技巧,而是一种设计思想,主要用于指引框架的设计。
依赖注入(Dependency Injection,DI)
控制反转是一种设计思想,而依赖注入则是一种编程技巧,同时依赖注入也是实现控制反转的一种方式。
了解清楚什么依赖注入,我们要先搞清楚几个概念:
调用方
:在编程语法角度,是对象或者类;在服务调用角度,是客户端;依赖
:在编程语法角度,变量;在服务调用角度,是服务;注入
:将“依赖”传递给“调用方”的过程,一般可以通过构造函数、setter 方法等等;
依赖注入,是通过注入给予调用方所需。
为了你能更充分理解依赖注入,我们看看这个例子:
public interface MsgSender {
void sendMessage(String userId, String msg);
}
public class CustomizedMsgSender implements MsgSender {
public void sendMessage(String userId, String msg) {...}
}
// 非依赖注入实现方式
public class UserService {
private MsgSender msgSender;
public UserService() {
msgSender = new CustomizedMsgSender();
}
public void sendMessage(String userId, String msg) {
msgSender.send(userId, msg);
}
}
UserService userService = new UserService();
userService.sendMessage(...);
// 依赖注入实现方式
public class UserService {
private MsgSender msgSender;
// 通过构造器方式注入
public UserService(MsgSender msgSender) {
this.msgSender = msgSender;
}
public void sendMessage(String userId, String msg) {
msgSender.send(userId, msg);
}
}
MsgSender sender = new CustomizedMsgSender();
UserService userService = new UserService(sender);
userService.sendMessage(...);
依赖注入,实现起来很简单。上面的例子,通过依赖注入的方式,将依赖的类对象 MsgSender 注入进来,提高了代码的扩展性,也可以灵活地替换依赖的类。
依赖注入框架(DI Framework)
了解清楚什么是依赖注入后,我们看看依赖注入框架。
沿用上面的例子,采用依赖注入的方式,虽然我们不需要使用 hard code 的方式,在类内部通过 new 来创建 MsgSender 对象,但是创建、组装的工作只是移到上层,还是需要程序员自我实现,代码如下所示:
public class Demo {
public static void main(String[] args) {
MsgSender sender = new CustomizedMsgSender();
UserService userService = new UserService(sender);
userService.sendMessage(...);
}
}
类似于 MsgSender 的对象,在生产项目中,或许存在成百上千个,如果每个都是以上面的方式依赖注入,依靠程序员写代码去完成依赖注入,一来容易出错,而来开发成本相对比较高。类对象构造和依赖注入的操作,与业务无关,我们是否可以交由框架去完成?我相信,你想到了,依赖注入框架(DI Framework)就应运而生了。
在编写逻辑时,借助依赖注入框架提供的扩展方式,预留扩展点,配置依赖(需要创建的类对象)、类间依赖关系,然后依赖注入框架自动创建所需依赖,管理依赖的生命周期,在扩展点注入依赖等等原本皆由程序员完成的事情。
如果你比较熟悉 Java ,你肯定听过 Spring 框架就是实现了依赖注入的框架。流行的依赖注入也会有很多,如Google Guice、Pico Container、Butterfly Container 等等。
依赖反转原则(Dependency Inversion Principle, DIP)
前面讲述了控制反转、依赖注入、依赖注入框架,我们接下来看看依赖反转原则。
依赖反转原则,有时候也被称作依赖倒置原则。那么,什么是依赖反转原则呢?
依赖反转原则由 Robert Martin 提出,并且在数篇公开著作中被表述,包括论文《面向对象设计质量标准:对于依赖的分析》,以及一篇1996年出现在C++报道中的名为《依赖反转原则》的文章,和《敏捷软件开发,原则,模式和实践》,《C#中的敏捷原则,模式和实践》两本书。
在1996年出现在C++报道中的名为《依赖反转原则》的文章中提到:
A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.
翻译过来:
A. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
B. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
在传统方式,高层次的组件依赖于低层次的组件(从调用链上划分,调用方是高层,而被调用方式低层),一般来说不会有什么大问题,但是,在这种结构下,高层次的组件直接依赖于低层次的组件实现业务。对于低层次组件的依赖限制了高层次组件被重用的可行性。
依赖反转原则,是一种解耦形式,指引框架层面的设计,目的在于将高层次组件从低层次组件中解耦出来,便于不同层级的组件重用,促进了不同层级间的解耦。低层次组件是对高层次组件接口的具体实现,因此低层次包的编译依赖于高层次组件包,低层次组件依赖于高层次组件的需求抽象
,这颠倒了传统的依赖关系。
我们通过下图,描述一下依赖反转的依赖关系:
图1中,高层对象A依赖于底层对象B的实现;
图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。
在设计模式中,设备器模式
应用了依赖反转原则,高层组件定义了设备器接口,高层组件和低层组件都依赖同一个抽象接口,高层组件使用多态来调用低层组件,而多态调用的设配对象则由低层组件具体实现。
同时,我们在 Tomcat 和 Jetty 中也能看到依赖反转原则的身影。比如Tomcat,Web 应用程序代码部署在 Tomcat 容器下,Tomcat 容器是调用方(高层次组件),而 Web 应用则是被调用方(低层次组件),容器和应用间则是通用 Servlet 抽象依赖。这就是 Servlet 规范,Servlet 并不依赖 Tomcat 容器,也不具有 Web 应用程序的实现细节。