【Java设计模式 经典设计原则】五 SOLID-DIP依赖反转原则

简介: 【Java设计模式 经典设计原则】五 SOLID-DIP依赖反转原则

这篇Blog来聊一聊SOLID原则的最后一个:依赖反转原则。依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则

理解依赖反转原则

依赖反转原则的完整描述是:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions),所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层

这里可能比较难理解,因为实际项目开发,高层当然是依赖低层实现的,不调用怎么使用功能?这里需要说明的是依赖反转原则指导的是框架的设计,而且这个原则也可以辨析一下概念:

  • 所谓反转并不是指反过来低层要依赖高层,而是二者都依赖抽象,低层和高层共同依赖的抽象接口是一种解耦,也可以理解为一种契约,高层只关心抽象接口实现的功能是否满足自己的需求,低层则按照抽象接口的要求规范自己的实现,但只要符合接口规范,实现可以相对自由,而高层也不会因为直接依赖低层的实现受影响
  • 依赖倒置原则概念是高层次模块不依赖于低层次模块。看似在要求高层次模块,实际上是在规范低层次模块的设计。低层次模块提供的接口要足够的抽象、通用,在设计时需要考虑高层次模块的使用种类和场景。明明是高层次模块要使用低层次模块,对低层次模块有依赖性。现在反而低层次模块需要根据高层次模块来设计,出现了「倒置」的显现。这样做的好处是:高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换

举两个比较典型的例子:

  • JVM是高层模块,我们编写的Java代码是底层模块。JVM和Java代码没有直接的依赖关系,两者都依赖同一个抽象,也就是class字节码。字节码不依赖具体的JVM虚拟机和Java语言,而JVM虚拟机和Java依赖字节码规范。 这样做的好处就是JVM与Java高度解耦,Java语言可以替换成Groovy、Kotlin,只要能编译为字节码,符合虚拟机的执行规范
  • Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范

这两个例子体现了,我们要想让框架或者容器能调用我们的实现方法实现功能,我们就得依赖满足高层运行功能抽象规范去写自己的实现。这就是依赖倒置的一种体现了。

IOC、DI和DIP辨析

实际上在学习Spring的时候我花费了大量的精力在这两个概念上,为此写了一篇较长的Blog:【Spring学习笔记 一】Sping基本概念及理论基础

理解IOC思想

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

如下的测试例子中,非控制反转的方式所有逻辑都需要RD去写

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.");
    }
  }
}

而如果有了测试框架,则RD只需要关注测试代码的核心内容,不需要关注测试类是怎么创建和运行的:

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();
    }
  }

RD只需要编写测试内容并将其添加到框架预留的扩展点即可:

public class UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    // ... 
  }
}
// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();

理解DI实现

依赖注入是控制反转思想的一种具体实现,它是一种具体的编码技巧:不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。在发送通知的一种场景下,我们可以比较下非DI的方式:

// 非依赖注入实现方式
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 在外部可以随意初始化。在内部则只有一种初始化方式,而如果MessageSender 是抽象类或接口,那么这种效果更明显了,这个在聊OCP的时候也探讨过:

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在Spring中的一种应用就是,Spring在运行时通过反射动态的向某个对象提供它所需要的其他对象,而Spring也可以看做是一个依赖注入框架

总结一下

理不辨不明,IOC是一种指导框架设计的思想,通过控制反转接管大量与核心业务逻辑无关的内容,让RD能专注业务逻辑开发,按照规范将代码注册到框架预留扩展点即可。而DI是IOC思想的一种具体实现方式,主要用于外部创建对象注入给当前对象,Spring中对DI的使用则更加具体为在运行时通过反射创建外部依赖对象并注入当前对象,所以Spring是一种贯彻IOC思想的通过DI实现的一个框架。依赖反转是一种设计原则,也是指导框架设计的,它关注的是如何约束框架代码和业务代码的关系,高层(框架)的运行不依赖于底层(业务代码),而是底层依据高层的需求来设计自己的实现,这才是倒置的真实含义。

相关文章
|
22小时前
|
设计模式 Java 程序员
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
本系列文章聚焦于面向对象软件设计中的设计模式,旨在帮助开发人员掌握23种经典设计模式及其应用。内容分为三大部分:第一部分介绍设计模式的概念、UML图和软件设计原则;第二部分详细讲解创建型、结构型和行为型模式,并配以代码示例;第三部分通过自定义Spring的IOC功能综合案例,展示如何将常用设计模式应用于实际项目中。通过学习这些内容,读者可以提升编程能力,提高代码的可维护性和复用性。
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
|
24天前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
2月前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
54 0
[Java]23种设计模式
|
1月前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
2月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
2月前
|
设计模式 Java
Java设计模式
Java设计模式
34 0
|
2月前
|
缓存 Java 数据库
JAVA分布式CAP原则
JAVA分布式CAP原则
70 0
|
2月前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
35 0
|
2月前
|
设计模式 Java
Java设计模式之桥接模式
这篇文章介绍了Java设计模式中的桥接模式,包括桥接模式的目的、实现方式,并通过具体代码示例展示了如何分离抽象与实现,使得两者可以独立变化。
49 0