天天看点

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

文章目录

  • ​​依赖反转原则​​
  • ​​控制反转​​
  • ​​怎么理解“反转”?​​
  • ​​控制反转的好处​​
  • ​​控制反转实例​​
  • ​​依赖注入​​
  • ​​依赖注入实例​​
  • ​​依赖注入实例2​​
  • ​​参考资料​​

依赖反转原则

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

参考资料