【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实现的一个框架。依赖反转是一种设计原则,也是指导框架设计的,它关注的是如何约束框架代码和业务代码的关系,高层(框架)的运行不依赖于底层(业务代码),而是底层依据高层的需求来设计自己的实现,这才是倒置的真实含义。

相关文章
|
28天前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
197 2
|
28天前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
238 0
|
3月前
|
设计模式 缓存 Java
Java设计模式(二):观察者模式与装饰器模式
本文深入讲解观察者模式与装饰器模式的核心概念及实现方式,涵盖从基础理论到实战应用的全面内容。观察者模式实现对象间松耦合通信,适用于事件通知机制;装饰器模式通过组合方式动态扩展对象功能,避免子类爆炸。文章通过Java示例展示两者在GUI、IO流、Web中间件等场景的应用,并提供常见陷阱与面试高频问题解析,助你写出灵活、可维护的代码。
|
27天前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
240 35
|
27天前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
208 8
|
6月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
158 0
|
3月前
|
设计模式 安全 Java
Java设计模式(一):单例模式与工厂模式
本文详解单例模式与工厂模式的核心实现及应用,涵盖饿汉式、懒汉式、双重检查锁、工厂方法、抽象工厂等设计模式,并结合数据库连接池与支付系统实战案例,助你掌握设计模式精髓,提升代码专业性与可维护性。
|
3月前
|
设计模式 XML 安全
Java枚举(Enum)与设计模式应用
Java枚举不仅是类型安全的常量,还具备面向对象能力,可添加属性与方法,实现接口。通过枚举能优雅实现单例、策略、状态等设计模式,具备线程安全、序列化安全等特性,是编写高效、安全代码的利器。
|
8月前
|
设计模式 Java 数据安全/隐私保护
Java 设计模式:装饰者模式(Decorator Pattern)
装饰者模式属于结构型设计模式,允许通过动态包装对象的方式为对象添加新功能,提供比继承更灵活的扩展方式。该模式通过组合替代继承,遵循开闭原则(对扩展开放,对修改关闭)。
|
9月前
|
设计模式 架构师 Java
设计模式觉醒系列(01)设计模式的基石 | 六大原则的核心是什么?
本文介绍了设计模式的六大原则,包括单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)、依赖倒置原则(DIP)和迪米特法则。通过具体案例分析了每个原则的应用场景及优势,强调了这些原则在提升代码可维护性、可复用性、可扩展性和降低耦合度方面的重要作用。文章指出,设计模式的核心在于确保系统模块间的低耦合高内聚,并为后续深入探讨23个经典设计模式打下基础。