设计模式之美-课程笔记11-设计原则5-依赖反转
理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
- 依赖反转指的是谁跟谁的什么依赖被反转了?反转二字又如何理解?
- 控制反转和依赖注入是什么?说的是一回事吗?
- Spring中的IoC跟这些概念有什么关系?
控制反转(IOC)
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.");
}
}
}
上面的代码所有的流程都由程序员来控制,下面我们抽象出一个简单的框架:
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();
}
}
把这个简化版的测试框架引入到工程后,只需在框架预留的扩展点,也就是TestCase类中的doTest抽象函数中,填充具体的测试代码就可实现之前的功能了,完全不需要写负责执行流程的main函数了:
public class UserServiceTest extends TestCase {
@Override
public boolean doTest() {
// ...
}
}
// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();
上述就是一个典型的控制反转的例子。
- 框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上添加跟自己业务相关的代码,就可以利用框架来驱动整个流程的执行。
- 这里的控制,指的是对程序执行流程的控制,而反转,指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架后,程序的执行流程可以通过框架控制。流程的控制权从程序员反转到了框架。
- 控制反转是一个指导思想,不是一种具体的实现技巧。
依赖注入(DI)
-
Dependency injection,是一种具体的编码技巧。
-
不通过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.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); } } public class MessageSender { public void send(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.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 Framework)
-
在之前的依赖注入实现的MessageSender和Notification的例子中,虽然不需要在Notification中hard code MessageSender,但是程序员还是要在更上层去初始化和装配这个MessageSender对象:
public class Demo { public static final void main(String args[]) { MessageSender sender = new SmsSender(); //创建对象 Notification notification = new Notification(sender);//依赖注入 notification.sendMessage("13918942177", "短信验证码:2346"); } }
-
随着代码量变大,对象会变得很多,框架可以帮我们做这个初始化和注入的事情。我们只需配置一下需要创建的类对象,以及类之间的关系。
依赖反转原则(DIP)
- Dependency Inversion Principle,DIP:High-level modules shouldn't depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn't depend on details. Details depend on abstractions.
- 高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象互相依赖。除此之外,抽象不需要依赖具体实现细节,具体实现细节依赖抽象。
- 高层模块和低层模块:就是调用链上调用者是高层,被调用者属于低层。平时高层依赖低层是没问题的,这个DIP还是指导框架设计的。
- 举个Tomca这个Servlet容器的例子:Tomcat是运行Java Web的应用程序的容器,我们便洗的Web应用程序只需要部署在Tomcat容器下,便可以被Tomcat容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。