1. 概述

依赖倒置原则(Dependency Inversion Principle,简称 DIP)是面向对象设计中著名的 SOLID 原则之一。它强调高层模块不应依赖于底层模块,而应依赖于抽象;同时,抽象不应依赖于细节,细节应依赖于抽象

简单来说,DIP 的核心思想是通过抽象解耦高层组件与低层组件之间的直接依赖关系,从而提高系统的灵活性、可维护性和可测试性。

本文将通过 Java 8 和 Java 11 的不同实现方式,详细讲解 DIP 的实际应用,包括如何通过接口抽象、依赖注入以及 Java 模块系统(JPMS)来实现这一原则。


2. DIP ≠ 依赖注入(DI)或控制反转(IoC)

虽然 DIP 与 DI 和 IoC 常常被放在一起讨论,但它们之间有本质区别:

DIP 是设计原则
它强调模块之间的依赖应基于抽象而不是具体实现。

DI 是实现方式
DI 是一种编程技术,通过构造函数、方法参数等方式将依赖注入到组件中,而不是让组件自己创建依赖。

IoC 是架构风格
IoC 将程序流程的控制权交给框架或容器,比如 Spring 框架就是典型的 IoC 实现。

虽然它们不是 DIP 的具体实现,但在实践中它们常常与 DIP 配合使用,共同构建松耦合的系统。


3. DIP 的核心概念

Robert C. Martin 在其著作《Agile Software Development: Principles, Patterns, and Practices》中给出了 DIP 的正式定义:

  1. 高层模块不应依赖低层模块,两者都应依赖抽象。
  2. 抽象不应依赖细节,细节应依赖抽象。

3.1. 示例:StringProcessor 类的设计

我们来看一个简单的例子,理解不同设计方式对 DIP 的影响。

public class StringProcessor {
    
    private final StringReader stringReader;
    private final StringWriter stringWriter;
    
    public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
        this.stringReader = stringReader;
        this.stringWriter = stringWriter;
    }

    public void printString() {
        stringWriter.write(stringReader.getValue());
    }
}

根据 DIP 的要求,我们有以下几种设计方式:

设计方式 说明 是否符合 DIP
直接依赖具体类 StringReaderStringWriter 是具体类,StringProcessor 依赖它们
接口在低层模块 StringReaderStringWriter 是接口,定义在低层模块
接口与高层模块同包 接口与 StringProcessor 同包,由高层模块拥有
接口独立成包 接口独立成包,高层与低层都依赖接口

只有最后两种设计方式符合 DIP 的要求。

3.2. 抽象归属权的界定

在 DIP 实践中,抽象归属权的划分对系统设计影响很大:

  • 高层模块拥有抽象:接口与高层模块放在同一包中,由高层模块定义抽象协议。
  • 抽象独立成层:接口放在独立的包中,高层和低层都依赖这个包,从而实现更强的解耦。

3.3. 抽象层级的选择

选择合适的抽象层级是实现 DIP 成功的关键:

  • 抽象层级应贴近高层模块的领域:例如,CustomerService 依赖的 CustomerDao 应该是一个业务层面的抽象,而不是一个具体的文件读取类。
  • 抽象层级过低:如果 StringReader 是一个 File 类,那么它与 StringProcessor 的抽象层级不匹配,无法体现 DIP 的优势。

4. Java 8 中的 DIP 实现

我们以一个简单的客户信息处理服务为例,演示如何在 Java 8 中实现 DIP。

4.1. 高层模块与接口定义

public class CustomerService {

    private final CustomerDao customerDao;

    public CustomerService(CustomerDao customerDao) {
        this.customerDao = customerDao;
    }

    public Optional<Customer> findById(int id) {
        return customerDao.findById(id);
    }

    public List<Customer> findAll() {
        return customerDao.findAll();
    }
}
public interface CustomerDao {
    Optional<Customer> findById(int id);
    List<Customer> findAll();
}

4.2. 低层实现

public class SimpleCustomerDao implements CustomerDao {

    private final Map<Integer, Customer> customers = new HashMap<>();

    public SimpleCustomerDao(Map<Integer, Customer> customers) {
        this.customers.putAll(customers);
    }

    @Override
    public Optional<Customer> findById(int id) {
        return Optional.ofNullable(customers.get(id));
    }

    @Override
    public List<Customer> findAll() {
        return new ArrayList<>(customers.values());
    }
}

4.3. 单元测试

@Before
public void setUpCustomerServiceInstance() {
    var customers = new HashMap<Integer, Customer>();
    customers.put(1, new Customer("John"));
    customers.put(2, new Customer("Susan"));
    customerService = new CustomerService(new SimpleCustomerDao(customers));
}

@Test
public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() {
    assertThat(customerService.findById(1)).isInstanceOf(Optional.class);
}

这种实现方式中,CustomerService 依赖的是接口 CustomerDao,而具体实现类 SimpleCustomerDao 实现了该接口。这种结构清晰地体现了 DIP 的核心思想。

direct-dip

4.4. 另一种实现方式

我们也可以将接口独立成包,使高层模块与低层模块都依赖这个接口包。这种方式更灵活,也更容易替换实现。

alternative-dip


5. Java 11 中的模块化实现(JPMS)

Java 9 引入的模块系统(JPMS)为 DIP 提供了天然支持。我们可以将高层模块、抽象接口、低层实现分别封装在不同的模块中。

5.1. 模块结构

project base directory
|- com.baeldung.dip.services
   module-info.java
   |- CustomerService.java
|- com.baeldung.dip.daos
   module-info.java
   |- CustomerDao.java
|- com.baeldung.dip.daoimplementations 
   module-info.java 
   |- SimpleCustomerDao.java  
|- com.baeldung.dip.entities
   module-info.java
   |- Customer.java
|- com.baeldung.dip.mainapp 
   module-info.java 
   |- MainApplication.java

5.2. 高层模块定义

module com.baeldung.dip.services {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    uses com.baeldung.dip.daos.CustomerDao;
    exports com.baeldung.dip.services;
}

5.3. 接口模块定义

module com.baeldung.dip.daos {
    requires com.baeldung.dip.entities;
    exports com.baeldung.dip.daos;
}

5.4. 低层实现模块定义

module com.baeldung.dip.daoimplementations {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    provides com.baeldung.dip.daos.CustomerDao with com.baeldung.dip.daoimplementations.SimpleCustomerDao;
    exports com.baeldung.dip.daoimplementations;
}

5.5. 实体模块定义

module com.baeldung.dip.entities {
    exports com.baeldung.dip.entities;
}
public class Customer {
    private final String name;
    // constructor, getter, toString
}

5.6. 主程序模块

module com.baeldung.dip.mainapp {
    requires com.baeldung.dip.entities;
    requires com.baeldung.dip.daos;
    requires com.baeldung.dip.daoimplementations;
    requires com.baeldung.dip.services;
    exports com.baeldung.dip.mainapp;
}
public class MainApplication {
    public static void main(String args[]) {
        var customers = new HashMap<Integer, Customer>();
        customers.put(1, new Customer("John"));
        customers.put(2, new Customer("Susan"));
        CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers));
        customerService.findAll().forEach(System.out::println);
    }
}

输出结果:

Customer{name=John}
Customer{name=Susan}

5.7. 模块依赖关系图

module dependency 1


6. 总结

本文从 DIP 的定义出发,通过 Java 8 和 Java 11 的具体实现方式,展示了如何在项目中应用这一设计原则:

Java 8 实现:通过接口抽象和依赖注入,实现高层模块与低层模块的解耦
Java 11 实现:借助 JPMS 模块系统,将各层组件模块化,进一步提升系统的可维护性与可扩展性

DIP 的核心在于通过抽象来解耦,而 Java 的模块化机制为我们提供了更强大的工具来实现这一目标。无论是在传统项目还是模块化项目中,DIP 都是构建高质量系统不可或缺的设计原则。


原始标题:The Dependency Inversion Principle in Java | Baeldung