全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我
一、依赖反转原则
依赖反转原则:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类似。我们拿 Tomcat 这个 Servlet 容器作为例子来解释一下。
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
再举一个例子:JDBC其实也是一种DIP原则(依赖反转原则),各个数据库厂商自行实现驱动,实现高层模块和低层模块的划分。
我们使用spring框架的小伙伴,相信都知道“控制反转”、“依赖注入”,也许很多小伙伴知其然并不知其所以然,今天我们就先聊聊,到底什么是控制反转、依赖反转、依赖注入?
控制反转实例
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom 在学习 Java 的课程");
}
public void studyPythonCourse() {
System.out.println("Tom 在学习 Python 的课程");
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyJavaCourse();
tom.studyPythonCourse();
}
}
Tom后续再学习其他课程时,业务扩展,我们的代码要从底层到高层(调用层)一次修改代码。在 Tom 类中增加 studyAICourse()的方法,在高层也要追加调用。如此一来,系统发布以后,实际上是非常不稳定的,在修改代码的同时也会带来意想不到的风险。
我们可以优化接口:
public interface ICourse {
void study();
}
public class JavaCourse implements ICourse {
@Override
public void study() {
System.out.println("Tom 在学习 Java 课程");
}
}
public class PythonCourse implements ICourse {
@Override
public void study() {
System.out.println("Tom 在学习 Python 课程");
}
}
public class Tom {
public void study(ICourse course){
course.study();
}
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.study(new JavaCourse());
tom.study(new PythonCourse());
}
我们这时候再看来代码,Tom 的兴趣无论怎么暴涨,对于新的课程,我只需要新建一个类,通过传参的方式告诉 Tom,而不需要修改底层代码。实际上这是一种大家非常熟悉的方式,叫依赖注入。注入的方式还有构造器方式和 setter 方式。我们来看构造器注入方式:
public class Tom {
private ICourse course;
public Tom(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
public static void main(String[] args) {
Tom tom = new Tom(new JavaCourse());
tom.study();
}
根据构造器方式注入,在调用时,每次都要创建实例。那么,如果 Tom 是全局单例,则我们就只能选择用 Setter 方式来注入,继续修改 Tom 类的代码:
public class Tom {
private ICourse course;
public void setCourse(ICourse course) {
this.course = course;
}
public void study() {
course.study();
}
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.setCourse(new JavaCourse());
tom.study();
tom.setCourse(new PythonCourse());
tom.study();
}
二、控制反转
实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。
怎么理解“反转”?
相对于传统的面向过程编程实践而言
- 程序的流程控制权发生了转变
- 应用程序与第三方代码之间的调用关系发生了转变
- 反转前:我们自己的代码决定程序的工作流程,并调用第三方代码(我们自己的代码是甲方,第三方代码是乙方)
- 反转后:第三方代码(框架)决定程序的工作流程,并调用我们写的代码(我们自己的代码是乙方,第三方代码是甲方)
控制反转的好处
好处也是很直接的,那就是复用。
复用代码有三种方式:类库、框架、设计模式。
- 类库:强调代码复用;
定义一组可复用的代码,供其他程序调用——拿来主义,别人的东西拿来用,用别人的锤子砸核桃。 - 框架:强调设计复用;
定义程序的体系结构,开发人员通过预留的接口插入代码(做填空题)——把自己的锤子装在流水线上,让它砸核桃。 - 设计模式:复用解决方案;
设计模式提供了解决一类问题的有效经验,复用这些经验往往可以很好地解决问题——看别人是怎么砸核桃的,依葫芦画瓢模仿一遍。
控制反转实例
我们日常工作中,相信以下代码大家非常熟悉了:
// 框架中的工具类
public class xxxxUtils {
public static boolean doSomething() {
// ... 框架中的固定方法
}
}
// 需要自定义逻辑
public class UserServiceTest {
public static void main(String[] args) {
if (doSomething()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
}
在上面的代码中,所有的流程都由程序员来控制。只有核心的代码可能会需要调用封装好的方法来执行。
我们再看看控制反转下,如何实现该实例:
// 框架中的类
public abstract class xxxxHandler {
public void run() {
if (doSomething()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
public abstract boolean doSomething();
}
// 自己定义的实现
public class MyHandler extends xxxxHandler {
@Overried
public boolean doSomething() {
// ...我自己的业务逻辑
};
}
// 框架中的初始化
public class Application {
private static final List<xxxxHandler> handlers= new ArrayList<>();
public static void register(xxxxHandler handler) {
handlers.add(handler);
}
// 启动类或者配置类
public static final void main(String[] args) {
for (xxxxHandler handler: handlers) {
handler.doSomething();
}
}
现在,我们只需要在框架预留的扩展点中扩展,继承框架中的父类,然后实现其中需要自定义的业务实现,然后注册到框架中即可,完全不需要关心框架是如何处理的:
// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
Application.register(new MyHandler();
框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。
实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有马上要讲到的依赖注入等方法,所以,控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。
三、依赖注入
依赖注入其实很简单:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
比如说Spring的Bean容器就是提前将所有的类对象创建好,在需要的时候直接注入使用。
依赖注入实例
// 非依赖注入实现方式
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 定义成接口,基于接口而非实现编程。改造后的代码如下所示:
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);
依赖注入实例2
我们代码中通过 Kafka 来发送异步消息。对于这样一个功能的开发,我们要学会将其抽象成一组跟具体消息队列(Kafka)无关的异步消息接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。具体代码如下所示:
// 这一部分体现了抽象意识
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}
public interface MessageFromatter { //... }
public class JsonMessageFromatter implements MessageFromatter {//...}
public class ProtoBufMessageFromatter implements MessageFromatter {//...}
public class Demo {
private MessageQueue msgQueue; // 基于接口而非实现编程
public Demo(MessageQueue msgQueue) { // 依赖注入
this.msgQueue = msgQueue;
}
// msgFormatter:多态、依赖注入
public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
//...
}
}
由此可见,基于接口的依赖注入有着很强的灵活性。
参考资料
王争老师《设计模式之美》
https://blog.csdn.net/sinat_36817189/article/details/12341028