设计原则05:控制反转原则(DIP)

前面的四节课分别讲了SOLID原则中的单一职责、开闭、里式替换、接口隔离原则,今天要讲的是依赖反转原则(DIP)。

先来看下这几个问题。

  • “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?
  • 我们还经常听到另外两个概念:“控制反转”和“依赖注入”。这两个概念跟“依赖反转”有什么区别和联系呢?它们说的是同一个事情吗?
  • 那 Spring 框架中的 IOC 跟这些概念又有什么关系呢?

带着这些问题,我们进入今天的学习。

控制反转(IOC,Inversion Of Control)

我们先来看一段代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.");
    }
  }
}

在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出这样一个框架,我们再来看如何利用框架来实现同样的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()抽象函数就可以实现之前的功能了。

1
2
3
4
5
6
7
8
9
public class UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    // ... 
  }
}

// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();

这个例子,就是典型的通过框架来实现“控制反转”的例子。框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要在预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。

这里的控制指的是对程序执行流程的控制,而反转指的是在没有使用框架之前,程序员自己控制整个流程的执行,在使用框架之后,整个程序的执行流程可以通过框架来控制了,流程的控制权由程序员“反转”到了框架。

实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有马上要讲到的依赖注入等方法,所以,控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

依赖注入(DI,Dependency Injection)

**依赖注入与控制反转恰恰相反,它是一种具体的编码技巧。**概括起来就是,不通过new()的方式在类的内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传入(或注入)给类使用。

举个例子。我们用Notification类负责消息推送,依赖MessageSender类实现推送商品促销、验证码等消息给用户。我们分别用依赖注入和非依赖注入来实现一下。

依赖注入的实现方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 依赖注入的实现方式
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);

非依赖注入的实现方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

// 非依赖注入实现方式
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();

通过依赖注入的方式将依赖的类对象传递进来,这样提高的代码的扩展性,我们可以灵活替换以来的类。当然,上面代码还有继续优化的空间,我们可以把MessageSender定义成接口,基于接口而非实现编程。改造后的代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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)

在上一节的例子中,当我们去发送消息时,仍然需要程序员来自己实现类的创建和组装,仅仅使用依赖注入实现了Notification类,事实上创建对象、组装(或注入)对象的工作仅仅是被移动到了更上层代码而已。

1
2
3
4
5
6
7
8

public class Demo {
  public static final void main(String args[]) {
    MessageSender sender = new SmsSender(); //创建对象
    Notification notification = new Notification(sender);//依赖注入
    notification.sendMessage("13918942177", "短信验证码:2346");
  }
}

在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。

你可能已经猜到,这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。实际上,现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。

依赖反转原则(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.

大体翻译如下:

高层模块(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 规范。

updatedupdated2023-06-032023-06-03
加载评论