这篇Blog来聊一聊SOLID原则的最后一个:依赖反转原则。依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则
理解依赖反转原则
依赖反转原则的完整描述是:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions),所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层
这里可能比较难理解,因为实际项目开发,高层当然是依赖低层实现的,不调用怎么使用功能?这里需要说明的是依赖反转原则指导的是框架的设计,而且这个原则也可以辨析一下概念:
- 所谓反转并不是指反过来低层要依赖高层,而是二者都依赖抽象,低层和高层共同依赖的抽象接口是一种解耦,也可以理解为一种契约,高层只关心抽象接口实现的功能是否满足自己的需求,低层则按照抽象接口的要求规范自己的实现,但只要符合接口规范,实现可以相对自由,而高层也不会因为直接依赖低层的实现受影响
- 依赖倒置原则概念是高层次模块不依赖于低层次模块。看似在要求高层次模块,实际上是在规范低层次模块的设计。低层次模块提供的接口要足够的抽象、通用,在设计时需要考虑高层次模块的使用种类和场景。明明是高层次模块要使用低层次模块,对低层次模块有依赖性。现在反而低层次模块需要根据高层次模块来设计,出现了「倒置」的显现。这样做的好处是:高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换
举两个比较典型的例子:
- JVM是高层模块,我们编写的Java代码是底层模块。JVM和Java代码没有直接的依赖关系,两者都依赖同一个抽象,也就是class字节码。字节码不依赖具体的JVM虚拟机和Java语言,而JVM虚拟机和Java依赖字节码规范。 这样做的好处就是JVM与Java高度解耦,Java语言可以替换成Groovy、Kotlin,只要能编译为字节码,符合虚拟机的执行规范
- Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范
这两个例子体现了,我们要想让框架或者容器能调用我们的实现方法实现功能,我们就得依赖满足高层运行功能抽象规范去写自己的实现。这就是依赖倒置的一种体现了。
IOC、DI和DIP辨析
实际上在学习Spring的时候我花费了大量的精力在这两个概念上,为此写了一篇较长的Blog:【Spring学习笔记 一】Sping基本概念及理论基础
理解IOC思想
控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。
如下的测试例子中,非控制反转的方式所有逻辑都需要RD去写
public class UserServiceTest { public static boolean doTest() { // ... } public static void main(String[] args) {//这部分逻辑可以放到框架中 if (doTest()) { System.out.println("Test succeed."); } else { System.out.println("Test failed."); } } }
而如果有了测试框架,则RD只需要关注测试代码的核心内容,不需要关注测试类是怎么创建和运行的:
public abstract class TestCase { public void run() { if (doTest()) { System.out.println("Test succeed."); } else { System.out.println("Test failed."); } } public abstract boolean 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 case: testCases) { case.run(); } }
RD只需要编写测试内容并将其添加到框架预留的扩展点即可:
public class UserServiceTest extends TestCase { @Override public boolean doTest() { // ... } } // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register() JunitApplication.register(new UserServiceTest();
理解DI实现
依赖注入是控制反转思想的一种具体实现,它是一种具体的编码技巧:不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。在发送通知的一种场景下,我们可以比较下非DI的方式:
// 非依赖注入实现方式 public class Notification { private MessageSender messageSender; public Notification() { this.messageSender = new MessageSender(); //此处有点像hardcode } public void sendMessage(String cellphone, String message) { //...省略校验逻辑等... this.messageSender.send(cellphone, message); } } public class MessageSender { public void send(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.send(cellphone, message); } } //使用Notification MessageSender messageSender = new MessageSender(); Notification notification = new Notification(messageSender);
很明显的可以看出,这大大提高了代码扩展性,因为MessageSender 在外部可以随意初始化。在内部则只有一种初始化方式,而如果MessageSender 是抽象类或接口,那么这种效果更明显了,这个在聊OCP的时候也探讨过:
public class Notification { private MessageSender messageSender; public Notification(MessageSender messageSender) { this.messageSender = messageSender; } public void sendMessage(String cellphone, String message) { this.messageSender.send(cellphone, message); } } public interface MessageSender { void send(String cellphone, String message); } // 短信发送类 public class SmsSender implements MessageSender { @Override public void send(String cellphone, String message) { //.... } } // 站内信发送类 public class InboxSender implements MessageSender { @Override public void send(String cellphone, String message) { //.... } } //使用Notification MessageSender messageSender = new SmsSender(); Notification notification = new Notification(messageSender);
DI在Spring中的一种应用就是,Spring在运行时通过反射动态的向某个对象提供它所需要的其他对象,而Spring也可以看做是一个依赖注入框架
总结一下
理不辨不明,IOC是一种指导框架设计的思想,通过控制反转接管大量与核心业务逻辑无关的内容,让RD能专注业务逻辑开发,按照规范将代码注册到框架预留扩展点即可。而DI是IOC思想的一种具体实现方式,主要用于外部创建对象注入给当前对象,Spring中对DI的使用则更加具体为在运行时通过反射创建外部依赖对象并注入当前对象,所以Spring是一种贯彻IOC思想的通过DI实现的一个框架。依赖反转是一种设计原则,也是指导框架设计的,它关注的是如何约束框架代码和业务代码的关系,高层(框架)的运行不依赖于底层(业务代码),而是底层依据高层的需求来设计自己的实现,这才是倒置的真实含义。