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

你好,我是猿java。

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

什么是依赖倒置?

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

1
2
3
4
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.

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

直接依赖的问题

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

img.png

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 高层模块: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。

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

如何实现依赖倒置?

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

方案一

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

img.png

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

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

img.png

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

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
// 高层模块: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数据库保存订单
}
}

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

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

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

方案二

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

img.png

因此,上述电商示例的依赖关系会变成下图:
img.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,持续输出硬核文章。

drawing