设计模式之美——依赖反转原则

Posted iblade

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之美——依赖反转原则相关的知识,希望对你有一定的参考价值。

依赖反转原则用起来比较简单,但概念理解起来比较难。

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

控制反转(IOC)

在讲“依赖反转原则”之前,我们先讲一讲“控制反转”。控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。此处强调一下, Java中,暂时别把这个“IOC”跟 Spring 框架的 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();

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

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

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

依赖注入(DI)

接下来,我们再来看依赖注入。依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。依赖注入的英文翻译是 Dependency Injection,缩写为 DI。对于这个概念,有一个非常形象的说法,那就是:依赖注入是一个标价 25 美元,实际上只值 5 美分的概念。也就是说,这个概念听起来很“高大上”,实际上,理解、应用起来非常简单。

那到底什么是依赖注入呢?我们用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。


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

实际上,你只需要掌握刚刚举的这个例子,就等于完全掌握了依赖注入。尽管依赖注入非常简单,但却非常有用,它是编写可测试性代码最有效的手段。

依赖注入是实现代码可测试性的最有效的手段:通过DI实现反转,将对象的创建交给业务调用方,这样就可以随意控制输出的结果,从而达到"mock"数据的目的,这样的思路太赞了。。。(补充下:不存在外部依赖的类对象可以直接通过new来创建)

依赖反转原则(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 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

以上是关于设计模式之美——依赖反转原则的主要内容,如果未能解决你的问题,请参考以下文章

Spring相关概念

我的《冒号课堂》学习笔记设计原则依赖原则

深入理解DIPIoCDI以及IoC容器

设计模式原则

面向对象设计原则四:依赖倒置原则

前端需要掌握的常见设计模式