SOLID设计原则:依赖倒置原则

简介: 本文介绍了依赖倒置原则,即高层模块不依赖低层模块,而是共同依赖抽象。直接依赖会导致紧耦合、难以测试和重用性差等问题。通过引入抽象层或独立抽象组件包,可以实现依赖倒置,提高系统灵活性和可维护性。Spring 和 Java SPI 是依赖倒置原则的典型应用。遵循该原则有助于设计更灵活、可扩展的系统架构。

你好,我是猿java。

当我们需要某个类A中使用到另外一个类B时,最直接的方式就是在A中直接依赖B,但是,今天我们要讲解的主角却是反其道而行之,它就是依赖倒置原则,那么,什么是依赖倒置原则?这种反向思维可以带来什么收益?这篇文章就来聊一聊。

什么是依赖倒置?

依赖倒置原则,英文为:Dependency inversion principle(简称DIP),也是 Robert C. Martin提出的 SOLID原则中的一种,老规矩,还是先看看作者 Robert C. Martin 对接口依赖倒置原则是如何定义的:

The Dependency Inversion Principle (DIP) states that high-level
modules should not depend on low-level modules; both should 
depend on abstractions. Abstractions should not depend on details.
Details should depend upon abstractions.

通过作者对依赖倒置的定义,可以总结出其核心思想是:高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该取决于抽象。

直接依赖的问题

对于上述依赖倒置的定义,如何理解呢?我们先来看下传统这种直接依赖会存在什么问题?如下为一张直接依赖的关系图:

DIP-direct-dependency.png

在上图中,高层组件 ObjectA直接依赖于低层组件 ObjectB,高层组件的重用机会受到限制,因为任何对低层组件的更改都会直接影响高层组件。

为了更好的说明直接依赖的问题,这里以一个真实的电商场景为例进行说明,其中有一个高层模块 OrderService用于处理订单,这个高层模块依赖于一个低层模块 OrderRepository来存储和检索订单数据。示例代码如下:

// 高层模块:OrderService
public class OrderService {
   
    private MySQLOrderRepository mySQLRepository;

    public OrderService(MySQLRepository mySQLRepository) {
   
        this.mySQLRepository = mySQLRepository;
    }

    public void createOrder(Order order) {
   
        // 一些业务逻辑
        mySQLRepository.save(order);
    }
}

// 低层模块:MySQLRepository
public class MySQLRepository {
   
    public void save(Order order) {
   
        // 使用 MySQL数据库保存订单
    }
}

在上述例子中,OrderService直接依赖于 OrderRepository,这种设计存在几个缺点:

  • 紧耦合:如果要把数据库从 MySQL切换到其他的数据库,我们需要修改 OrderService,因为它直接依赖于 OrderRepository。
  • 难以测试:在进行单元测试时,我们无法轻松地对 OrderService 进行模拟,因为它直接依赖于具体实现 MySQLRepository。
  • 重用性差:如果在另一个项目中我们需要使用 OrderService 但存储订单的方式不同,例如使用文件系统或远程服务,我们将无法直接重用 OrderService。

那么,对于这些缺点,该如何解决呢?接下来我们将重点讲解。

如何实现依赖倒置?

这里提供两种主流的解决方案。

方案一

通过低级组件实现高级组件的接口,要求低级组件包依赖于高级组件进行编译,从而颠倒了传统的依赖关系,如下图:

DIP.png

图1中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。

因此,上面的问题我们也可以通过引入一个抽象层 OrderRepository来解耦高层模块和低层模块,整个关系图如下:

DIP-abstract-interface.png

通过这种方式,OrderService依赖于 OrderRepository接口而不是具体实现 MySQLRepository。这样,我们可以轻松替换低层实现而无需修改高层模块,修改后的代码如下:

// 高层模块:OrderService
public class OrderService {
   
    private OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
   
        this.orderRepository = orderRepository;
    }

    public void placeOrder(Order order) {
   
        // 一些业务逻辑
        orderRepository.save(order);
    }
}

// 抽象层:OrderRepository接口
public interface OrderRepository {
   
    void save(Order order);
}

// 低层模块:MySQLRepository实现
public class MySQLRepository implements OrderRepository {
   
    public void save(Order order) {
   
        // 使用MySQL数据库保存订单
    }
}

// 另一个低层模块:PostgreSQLRepository实现
public class PostgreSQLRepository implements OrderRepository {
   
    public void save(Order order) {
   
        // 使用PostgreSQL数据库保存订单
    }
}

在应用程序中,我们可以灵活选择使用哪种具体实现,也可以把数据库的选择做成配置:

OrderRepository orderRepository = new MySQLRepository(); // 或 new PostgreSQLRepository();
OrderService orderService = new OrderService(orderRepository);

通过这种方式,OrderService变得更具重用性、可测试性更强,并且与具体的存储实现解耦,满足依赖倒置原则的要求。

方案二

尽管方式一也实现了依赖倒置,但是这种实现方式高层组件以及组件是封装在一个包中,对低层组件的重用会差一些,因此,另一种更灵活的解决方案是将抽象组件提取到一组独立的包/库中,如下图:

DIP-abstract-interface2-1.png

因此,上述电商示例的依赖关系会变成下图:
DIP-abstract-interface2.png

这种实现方式将每一层分离成自己的封装,鼓励任何层的再利用,提供稳健性和移动性。

两种方案的核心思想都是一样的,只是在灵活性和组件复用的考虑上略有差异。

依赖倒置的实例

在 Java语言中,使用依赖倒置原则的框架或者技术点有很多,这里列举2个比较较常用的例子:

Spring

Spring框架的核心之一是依赖注入(Dependency Injection, DI),这是依赖倒置原则的一个实现。通过Spring容器管理对象的创建和依赖关系,可以使得高层模块和低层模块都依赖于抽象。Spring支持构造器注入、setter注入和接口注入等多种方式。

Java SPI

Java SPI(Service Provider Interface)机制也体现了依赖倒置原则,SPI机制通过定义接口和服务提供者(Service Providers),使得高层模块(使用者)和低层模块(提供者)之间的依赖关系可以通过接口进行解耦。具体来说,高层模块依赖于抽象(接口),而不是具体的实现,从而实现了依赖倒置原则。

JDBC(Java Database Connectivity)就是使用 SPI机制来加载和注册数据库驱动程序,使得应用程序可以动态地使用不同的数据库而无需修改代码。

JDBC SPI的工作原理:

  • 定义服务接口:JDBC API定义了一组接口,如 java.sql.Driver。
  • 实现服务接口:每个数据库厂商实现这些接口,例如,MySQL的驱动实现了 java.sql.Driver接口。
  • 声明服务提供者:数据库驱动的JAR包中包含一个文件,声明实现类。
  • 加载服务提供者:通过 ServiceLoader或 JDBC API动态加载并实例化驱动实现。

总结

本文通过一个电商示例分析了什么是依赖倒置原则,并且提出了依赖倒置的两种实现风格,通过引入抽象层,可以降低系统的耦合度,提升系统的扩展性和可维护性。因此,在实际开发中,我们应当始终遵循依赖倒置原则,设计灵活、可扩展的系统架构,从而应对复杂多变的业务需求。

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注:猿java,持续输出硬核文章。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
iOS开发 MacOS
还在为 iTerm 多窗口操作烦恼?tmux 这款神器轻松帮你解决(下)
粉在之前文章中教过大家如何结合 zsh 让 iterm2 发挥最佳效果。 什么还没有看过?赶紧看下补一下前提知识:收集了这么多实用技巧,帮助你的 iterm2 成为最帅的那个! 上篇文中,阿粉提到每次上线发布的时候,都会打开很多 iTerm 窗口,通过 tab 页拖拽方式让所有窗口可以同时显示。
2112 0
还在为 iTerm 多窗口操作烦恼?tmux 这款神器轻松帮你解决(下)
|
开发工具 git
Git - Smart Checkout、Force Checkout 区别
Git - Smart Checkout、Force Checkout 区别
3184 0
Git - Smart Checkout、Force Checkout 区别
|
7月前
|
SQL Java 测试技术
告别 CRUD 泥沼!DDD 领域驱动设计:从底层原理到生产级全链路落地实战
DDD是应对复杂业务的架构思想,核心是“领域优先、边界隔离”:通过战略设计(统一语言、限界上下文、上下文映射)划清业务边界;通过战术设计(实体/值对象、聚合根、领域服务等)落地高内聚、低耦合的代码。非银弹,适用于规则多、迭代快、协作难的场景。
1786 1
|
10月前
|
NoSQL Java 数据库连接
SpringBoot框架
SpringBoot简化Spring开发,核心功能包括starter起步依赖、自动配置及内嵌服务器支持。通过@SpringBootApplication实现自动化配置,支持多种配置方式,优先级为:命令行参数 > 系统属性 > properties > yml/yaml。可自定义starter实现模块化集成。
|
存储 缓存 关系型数据库
【MySQL调优】如何进行MySQL调优?一篇文章就够了!
MySQL调优主要分为三个步骤:监控报警、排查慢SQL、MySQL调优。 排查慢SQL:开启慢查询日志 、找出最慢的几条SQL、分析查询计划 。 MySQL调优: 基础优化:缓存优化、硬件优化、参数优化、定期清理垃圾、使用合适的存储引擎、读写分离、分库分表; 表设计优化:数据类型优化、冷热数据分表等。 索引优化:考虑索引失效的11个场景、遵循索引设计原则、连接查询优化、排序优化、深分页查询优化、覆盖索引、索引下推、用普通索引等。 SQL优化。
【MySQL调优】如何进行MySQL调优?一篇文章就够了!
什么是依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP)是面向对象设计中的SOLID原则之一,强调高层模块不应依赖低层模块,双方应依赖于抽象。该原则包含两方面:抽象不依赖细节,细节依赖抽象。这有助于降低耦合度、提高模块化和灵活性。实践中可通过接口定义契约、依赖注入等方式实现。例如,在Java中定义`MessageService`接口及其实现`EmailService`,高层`NotificationService`依赖于`MessageService`接口而非具体实现,从而实现了对扩展开放、对修改关闭的设计目标。
953 1
|
人工智能 缓存 Kubernetes
Kubernetes在AI/大数据领域的 最新进展与大规模实践
根据在[CNCC 2022开源分布式云原生系统技术发展](https://ccf.org.cn/cncc2022/schedule_d_4075)论坛的演讲整理。 ![](https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/08cde176-d1d3-49ae-a1f7-cc025d3f06f3.png) 在云计算技术快速发展的过
2997 73
Kubernetes在AI/大数据领域的最新进展与大规模实践
Mac每次都要执行source ~/.bash_profile配置的环境变量才生效
Mac每次都要执行source ~/.bash_profile配置的环境变量才生效
1403 2
|
存储 前端开发 数据库
MVC模式和三层架构
MVC模式和三层架构
|
NoSQL Java 数据库
重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验
重复点击提交、产生多笔数据、保持数据只操作一次---->接口幂等性校验
683 0