10k

设计模式之美-课程笔记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();

上述就是一个典型的控制反转的例子。

  1. 框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上添加跟自己业务相关的代码,就可以利用框架来驱动整个流程的执行。
  2. 这里的控制,指的是对程序执行流程的控制,而反转,指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架后,程序的执行流程可以通过框架控制。流程的控制权从程序员反转到了框架。
  3. 控制反转是一个指导思想,不是一种具体的实现技巧。

依赖注入(DI)

  1. Dependency injection,是一种具体的编码技巧。

  2. 不通过new()的方式在类内部依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(注入)给类使用。

  3. 看一个例子吧: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);
通过依赖注入的方式将依赖的类传递进来,提高了代码的扩展性。
  1. 上述代码还可以优化,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)

  1. 在之前的依赖注入实现的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");
      }
    }
  1. 随着代码量变大,对象会变得很多,框架可以帮我们做这个初始化和注入的事情。我们只需配置一下需要创建的类对象,以及类之间的关系。

依赖反转原则(DIP)

  1. 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.
  2. 高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象互相依赖。除此之外,抽象不需要依赖具体实现细节,具体实现细节依赖抽象。
  3. 高层模块和低层模块:就是调用链上调用者是高层,被调用者属于低层。平时高层依赖低层是没问题的,这个DIP还是指导框架设计的。
  4. 举个Tomca这个Servlet容器的例子:Tomcat是运行Java Web的应用程序的容器,我们便洗的Web应用程序只需要部署在Tomcat容器下,便可以被Tomcat容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
Thoughts? Leave a comment

Comments
  1. Anonymous — Jun 20, 2023:

    interesting