前言
今天学习 SOLID 中的最后一个原则,依赖反转原则。
本章内容,可以带着如下几个问题:
- “依赖反转” 这个概念指的是 “谁跟谁” 的 “什么依赖” 被反转了? “反转” 这两个字该如何理解。
- 我们还经常听到另外两个概念:“控制反转” 和 “依赖注入”。这两个概念跟 “依赖反转” 有什么区别和联系吗?它们说的是同一个事情吗?
- 如果你熟悉 Java 语言,那 Spring 框架中的 IOC 跟这些概念有什么关系?
控制反转(IOC)
在讲依赖反转原则之前,我们先讲下“控制反转”。
如果你是Java工程师的话,暂时别把这个 “IOC” 和 Spring 框架的 “IOC” 联系在一起。关于 Spring 的 IOC ,我们待会还会讲到。
先通过一个例子来查看,什么是控制反转。
public class UserServiceTest {public static final boolean doTest() {// ...}public static void main(String[] args) { // 这部分逻辑可以放到框架中if (doTest()) {System.out.println("Test succeeded.");} else {System.out.println("Test failed.");}}
}
在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出下面这样一个框架,再来看,如何利用框架来实现同样的功能。
public abstract class TestCase {public void run() {if (doTest()) {System.out.println("Test succeeded.");} else {System.out.println("Test failed.");}}public abstract void doTest();
}public class JunitApplication() {private static final List<TestCase> testCases = new ArrayList<>();public static void register(TestCase testCase) {testCases.add(testCase);}public static final void main(String[] args) {for (TestCase testCase : testCases) {testCase.run();}}
}
这个简化版本的测试框架引入到工程之后,我们只需要在框架预留的扩展点,即 TestCase
类中的 doTest()
抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的 main()
函数了。具体代码如下所示:
public class UserServiceTest extends TestCase {@Overridepublic void doTest() {// ...}
}// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用 register()
JunitApplication.register(new UserServiceTest());
上面例子,就是典型的通过框架来实现“控制反转”的例子。框架提供了一个可扩展的代码骨架,用来封装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架驱动整个程序流程的执行。
- 这里的“控制” 指的是对程序执行流程的控制。
- 而“反转”是指流程的控制权从程序员“反转”到了框架。在没有使用框架前,程序员自己控制整个程序的执行;使用框架后,整个执行流程可以通过框架来控制。
实际上,实现控制反转的方法有很多,除了上面的例子中类似模板设计模式的方法之外,还有依赖注入等方法。所以,控制反转并不是一种具体的实现技巧,而是一种比较笼统的设计思想,一般用来指导框架层面的设计。
依赖注入(DI)
依赖注入和控制反转相反,它是一种编码技巧。
依赖注入英文翻译为:Dependency Injection,缩写为 DI。
什么是依赖注入? 用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类的对象,而是将依赖类对象在外部建好之后,通过构造函数、函数参数等方式传递(或注入)给类适用。
通过一个例子来解释下。在这个例子中, Notification
类负责消息推送,依赖 MessageSender
类来实现推送商品促销、验证码等消息给用户。我们分别用依赖注入和非依赖注入两种方式来实现一下。具体的实现代码如下所示:
// 非依赖注入实现方式
public class Notification {private MessageSender messageSender;public Notification() {this.messageSender = new MessageSender(); // 此处有点像hardcode}public void sendMessage(String cellPhone, String message) {// 省略校验逻辑等...this.messageSender.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(String cellPhone, String message) {// ...}
}// 使用Notification
Notification notification = new Notification();// 依赖注入实现方式
public class Notification {private MessageSender messageSender;// 通过构造函数将messageSender传递进来public Notification(MessageSender messageSender) {this.messageSender = messageSender;}public void sendMessage(String cellPhone, String message) {// 省略校验逻辑等...this.messageSender.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(String cellPhone, String message) {// ...}
}// 使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);
通过依赖注入的方式将对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。这一点在我们之前将“开闭原则”的也提到过。当然,上面代码还有继续优化的空间,把 MessageSender 定义成接口,基于接口而非实现编程。改造后代码如下:
public class Notification {private MessageSender messageSender;public Notification(MessageSender messageSender) {this.messageSender = messageSender;}public void sendMessage(String cellPhone, String message) {this.messageSender.sendMessage(cellPhone, message);}
}public interface MessageSender {void sendMessage(String cellPhone, String message);
}public class SmsSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}public class InboxSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}// 使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);
实际上,只需掌握刚刚举的例子,就掌握了依赖注入。尽管依赖注入非常简单,但却非常有用。
依赖注入框架(DI Framework)
弄懂了什么是“依赖注入”,在来看下,什么“依赖注入框架”。还是借用刚刚例子来解释。
在采用依赖注入实现的 Notification
类中,虽然不需要使用类似 hard code 的方式,在类内部通过 new 来创建 MessageHandler
对象,但是这个创建对象、组装(或注入)对象的工作,仅仅是被移动到了上层代码而已,还是需要我们程序员自己来实现。具体代码如下:
public class Demo {public static void main(String[] args) {MessageSender messageSender = new SmsSender(); // 创建对象Notification notification = new Notification(messageSender); // 依赖注入notification.sendMessage("13910221123", "短信验证码:2345");}
}
在实际开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建的依赖注入会变得非常复杂。如果这部分工作由程序员自己写代码来完成,容易出错且开发成本比较高。而创建和依赖注入的工作,本身和业务无关,完全可以抽象成框架来自动完成。
这个框架就是“依赖注入框架”。只需要通过依赖注入框架提供的扩展点,简单配置以下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命这周期、依赖注入等事情。
实际上,现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container 等。
不过,Spring 框架自己声称是控制反转容器(Inversion of Control Container)。
实际上,这两种说法都没错。只是控制反转容器这种表述是一种非常宽泛的描述,除了依赖注入,还有模板模式等,而 Spring 框架的控制反转主要是通过依赖注入来实现的。
依赖反转原则(DIP)
接下来讲一下本章的主角:依赖反转原则。依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。有时也翻译成依赖倒置原则。
英文原文描述:
High-level modules shouldn’t depend on low-level modules。Both modules should depend on abstractions shouldn’t depend on details。Details depend on abstractions.
翻译成中文,大概意思是: 高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来相互依赖。此外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
所谓高层模块和低层模块的划分,简单来说,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模型是没有任何问题的。实际上,这条原则主要用来指导框架层面的设计,跟前面讲到的控制反转类似。我们拿 Tomcat 这个 Servlet 容器作为例子来解释下。
Tomcat 是运行 Java Web 应用程序的容器。我们编写 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。
- 按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序属于低层模块。
- Tomcat 和 应用程序之间并没有直接的依赖关系,两者依赖同一个抽象,也就是 Servlet 规范。
- Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节。
- 而 Tomcat 容器和应用程序依赖 Servlet 规范。
总结
1.控制反转
控制反转是一个比较抽象的设计思想,并不是具体的实现方法,一般指导框架层面的设计。
- “控制” 指的是对程序执行流程的控制。
- “反转” 指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程控制权从程序员 “反转” 给了框架。
2.依赖注入
依赖注入和控制反转相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖类的对象在外部创建好之后,通过构造函数、函数参数等方式注入给类适用。
3.依赖注入框架
我们通过依赖注入框架提供的扩展点,简单配置下需要的类及类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象生命周期、依赖注入等原本需要程序员来做的事情。
4.依赖反转原则
依赖反转原则,也叫依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。
- 高层模型不依赖低层模块
- 它们共同依赖同一个抽象
- 抽象不要依赖具体实现细节
- 具体实现细节依赖抽象。