当我们需要某个类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.
通过作者对依赖倒置的定义,可以总结出其核心思想是:高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该取决于抽象。
直接依赖的问题
对于上述依赖倒置的定义,如何理解呢?我们先来看下传统这种直接依赖会存在什么问题?如下为一张直接依赖的关系图:
在上图中,高层组件 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。
那么,对于这些缺点,该如何解决呢?接下来我们将重点讲解。
如何实现依赖倒置?
这里提供两种主流的解决方案。
方案一
通过低级组件实现高级组件的接口,要求低级组件包依赖于高级组件进行编译,从而颠倒了传统的依赖关系,如下图:
图1中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。
因此,上面的问题我们也可以通过引入一个抽象层 OrderRepository来解耦高层模块和低层模块,整个关系图如下:
通过这种方式,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变得更具重用性、可测试性更强,并且与具体的存储实现解耦,满足依赖倒置原则的要求。
方案二
尽管方式一也实现了依赖倒置,但是这种实现方式高层组件以及组件是封装在一个包中,对低层组件的重用会差一些,因此,另一种更灵活的解决方案是将抽象组件提取到一组独立的包/库中,如下图:
因此,上述电商示例的依赖关系会变成下图:
这种实现方式将每一层分离成自己的封装,鼓励任何层的再利用,提供稳健性和移动性。
两种方案的核心思想都是一样的,只是在灵活性和组件复用的考虑上略有差异。
依赖倒置的实例
在 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动态加载并实例化驱动实现。
总结
本文通过一个电商示例分析了什么是依赖倒置原则,并且提出了依赖倒置的两种实现风格,通过引入抽象层,可以降低系统的耦合度,提升系统的扩展性和可维护性。因此,在实际开发中,我们应当始终遵循依赖倒置原则,设计灵活、可扩展的系统架构,从而应对复杂多变的业务需求。