设计原则之【依赖反转原则】依赖反转、控制反转、依赖注入,都是什么意思?


全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

一、依赖反转原则

依赖反转原则:高层模块(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

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃了也弱了。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值