1. Overview
1.概述
The Dependency Inversion Principle (DIP) forms part of the collection of object-oriented programming principles popularly known as SOLID.
依赖反转原则(DIP)是面向对象编程原则集合的一部分,该原则被普遍称为SOLID。
At the bare bones, the DIP is a simple – yet powerful – programming paradigm that we can use to implement well-structured, highly-decoupled, and reusable software components.
从根本上说,DIP是一种简单而强大的编程范式,我们可以用它来实现结构良好、高度解耦和可重用的软件组件。
In this tutorial, we’ll explore different approaches for implementing the DIP — one in Java 8, and one in Java 11 using the JPMS (Java Platform Module System).
在本教程中,我们将探索实现DIP的不同方法–一种是在Java 8中,另一种是在Java 11中使用JPMS(Java平台模块系统)。
2. Dependency Injection and Inversion of Control Are Not DIP Implementations
2.依赖注入和反转控制不是DIP的实现
First and foremost, let’s make a fundamental distinction to get the basics right: the DIP is neither dependency injection (DI) nor inversion of control (IoC). Even so, they all work great together.
首先,也是最重要的,让我们做一个基本的区分,以正确地掌握基本知识。DIP是既不是依赖注入(DI)也不是反转控制(IoC)。即便如此,它们都能很好地协同工作。
Simply put, DI is about making software components to explicitly declare their dependencies or collaborators through their APIs, instead of acquiring them by themselves.
简单地说,DI就是让软件组件通过其API明确地声明它们的依赖关系或合作者,而不是自己获取它们。
Without DI, software components are tightly coupled to each other. Hence, they’re hard to reuse, replace, mock and test, which results in rigid designs.
在没有DI的情况下,软件组件彼此之间是紧密耦合的。因此,它们很难被重用、替换、模拟和测试,这导致了设计的僵化。
With DI, the responsibility of providing the component dependencies and wiring object graphs is transferred from the components to the underlying injection framework. From that perspective, DI is just a way to achieve IoC.
通过DI,提供组件依赖关系和布线对象图的责任从组件转移到了底层注入框架。从这个角度来看,DI只是实现IoC的一种方式。
On the other hand, IoC is a pattern in which the control of the flow of an application is reversed. With traditional programming methodologies, our custom code has the control of the flow of an application. Conversely, with IoC, the control is transferred to an external framework or container.
另一方面,IoC是一种模式,其中对应用程序的流程控制是相反的。在传统的编程方法中,我们的自定义代码拥有对应用程序流程的控制权。相反,通过IoC,控制权被转移到外部框架或容器中。
The framework is an extendable codebase, which defines hook points for plugging in our own code.
该框架是一个可扩展的代码库,它定义了用于插入我们自己代码的钩点。
In turn, the framework calls back our code through one or more specialized subclasses, using interfaces’ implementations, and via annotations. The Spring framework is a nice example of this last approach.
反过来,框架通过一个或多个专门的子类,使用接口的实现,以及通过注解来回调我们的代码。Spring框架是最后这种方法的一个很好的例子。
3. Fundamentals of DIP
3.DIP的基本原理
To understand the motivation behind the DIP, let’s start with its formal definition, given by Robert C. Martin in his book, Agile Software Development: Principles, Patterns, and Practices:
为了理解DIP背后的动机,让我们从Robert C. Martin在他的书中给出的正式定义开始,Agile Software Development。Principles, Patterns, and Practices。
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
So, it’s clear that at the core, the DIP is about inverting the classic dependency between high-level and low-level components by abstracting away the interaction between them.
因此,很明显,在核心方面,DIP是通过抽象化高层和低层组件之间的互动来颠倒它们之间的经典依赖关系。
In traditional software development, high-level components depend on low-level ones. Thus, it’s hard to reuse the high-level components.
在传统的软件开发中,高层组件依赖于低层组件。因此,很难重用高层组件。
3.1. Design Choices and the DIP
3.1.设计选择和DIP
Let’s consider a simple StringProcessor class that gets a String value using a StringReader component, and writes it somewhere else using a StringWriter component:
让我们考虑一个简单的StringProcessor类,它使用StringReader组件获得String值,并使用StringWriter组件将其写入其他地方。
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());
}
}
Although the implementation of the StringProcessor class is basic, there are several design choices that we can make here.
尽管StringProcessor类的实现是基本的,但我们可以在这里做出几个设计选择。
Let’s break each design choice down into separate items, to understand clearly how each can impact the overall design:
让我们把每个设计选择分解成独立的项目,以清楚地了解每个项目如何影响整个设计。
- StringReader and StringWriter, the low-level components, are concrete classes placed in the same package. StringProcessor, the high-level component is placed in a different package. StringProcessor depends on StringReader and StringWriter. There is no inversion of dependencies, hence StringProcessor is not reusable in a different context.
- StringReader and StringWriter are interfaces placed in the same package along with the implementations. StringProcessor now depends on abstractions, but the low-level components don’t. We have not achieved inversion of dependencies yet.
- StringReader and StringWriter are interfaces placed in the same package together with StringProcessor. Now, StringProcessor has the explicit ownership of the abstractions. StringProcessor, StringReader, and StringWriter all depend on abstractions. We have achieved inversion of dependencies from top to bottom by abstracting the interaction between the components. StringProcessor is now reusable in a different context.
- StringReader and StringWriter are interfaces placed in a separate package from StringProcessor. We achieved inversion of dependencies, and it’s also easier to replace StringReader and StringWriter implementations. StringProcessor is also reusable in a different context.
Of all the above scenarios, only items 3 and 4 are valid implementations of the DIP.
在上述所有情况中,只有第3项和第4项是对DIP的有效实施。
3.2. Defining the Ownership of the Abstractions
3.2.定义摘要的所有权
Item 3 is a direct DIP implementation, where the high-level component and the abstraction(s) are placed in the same package. Hence, the high-level component owns the abstractions. In this implementation, the high-level component is responsible for defining the abstract protocol through which it interacts with the low-level components.
第3项是直接的DIP实现,高层组件和抽象被放在同一个包里。因此,高层组件拥有抽象。在这种实现中,高层组件负责定义抽象协议,通过该协议与低层组件进行交互。
Likewise, item 4 is a more decoupled DIP implementation. In this variant of the pattern, neither the high-level component nor the low-level ones have the ownership of the abstractions.
同样地,第4项是一个更加解耦的DIP实现。在这个模式的变体中,高层组件和低层组件都没有对抽象的所有权。
The abstractions are placed in a separate layer, which facilitates switching the low-level components. At the same time, all the components are isolated from each other, which yields stronger encapsulation.
抽象被放置在一个单独的层中,这有利于切换低级别的组件。同时,所有的组件都是相互隔离的,这就产生了更强的封装性。
3.3. Choosing the Right Level of Abstraction
3.3.选择正确的抽象级别
In most cases, choosing the abstractions that the high-level components will use should be fairly straightforward, but with one caveat worth noting: the level of abstraction.
在大多数情况下,选择高层组件将使用的抽象应该是相当直接的,但有一个值得注意的问题:抽象的层次。
In the example above, we used DI to inject a StringReader type into the StringProcessor class. This would be effective as long as the level of abstraction of StringReader is close to the domain of StringProcessor.
在上面的例子中,我们用DI将一个StringReader类型注入到StringProcessor类。只要StringReader的抽象级别与StringProcessor的领域接近,这将是有效的。
By contrast, we’d be just missing the DIP’s intrinsic benefits if StringReader is, for instance, a File object that reads a String value from a file. In that case, the level of abstraction of StringReader would be much lower than the level of the domain of StringProcessor.
相比之下,如果StringReader是一个File对象,从文件中读取String值,我们就只是错过了DIP的内在好处。在这种情况下,StringReader的抽象水平将远远低于StringProcessor的领域水平。
To put it simply, the level of abstraction that the high-level components will use to interoperate with the low-level ones should be always close to the domain of the former.
简单地说,高层组件用来与低层组件互操作的抽象水平应该总是接近前者的领域。
4. Java 8 Implementations
4.Java 8的实现
We already looked in depth at the DIP’s key concepts, so now we’ll explore a few practical implementations of the pattern in Java 8.
我们已经深入研究了DIP的关键概念,所以现在我们将探索该模式在Java 8中的一些实际实现。
4.1. Direct DIP Implementation
4.1.直接实施DIP
Let’s create a demo application that fetches some customers from the persistence layer and processes them in some additional way.
让我们创建一个演示应用程序,从持久层获取一些客户,并以一些额外的方式处理它们。
The layer’s underlying storage is usually a database, but to keep the code simple, here we’ll use a plain Map.
该层的底层存储通常是一个数据库,但为了保持代码的简单,这里我们将使用一个普通的Map。
Let’s start by defining the high-level component:
让我们从定义高层组件开始。
public class CustomerService {
private final CustomerDao customerDao;
// standard constructor / getter
public Optional<Customer> findById(int id) {
return customerDao.findById(id);
}
public List<Customer> findAll() {
return customerDao.findAll();
}
}
As we can see, the CustomerService class implements the findById() and findAll() methods, which fetch customers from the persistence layer using a simple DAO implementation. Of course, we could’ve encapsulated more functionality in the class, but let’s keep it like this for simplicity’s sake.
我们可以看到,CustomerService类实现了findById()和findAll()方法,这些方法使用简单的DAO>实现从持久层获取客户。当然,我们可以在这个类中封装更多的功能,但为了简单起见,我们还是保持这个样子。
In this case, the CustomerDao type is the abstraction that CustomerService uses for consuming the low-level component.
在这种情况下,CustomerDao类型是CustomerService用于消费底层组件的抽象。
Since this a direct DIP implementation, let’s define the abstraction as an interface in the same package of CustomerService:
由于这是一个直接的DIP实现,让我们把这个抽象定义为CustomerService的同一个包中的一个接口。
public interface CustomerDao {
Optional<Customer> findById(int id);
List<Customer> findAll();
}
By placing the abstraction in the same package of the high-level component, we’re making the component responsible for owning the abstraction. This implementation detail is what really inverts the dependency between the high-level component and the low-level one.
通过把抽象放在高层组件的同一个包里,我们让组件负责拥有这个抽象。这个实现细节是真正颠倒了高层组件和低层组件之间的依赖关系。
In addition, the level of abstraction of CustomerDao is close to the one of CustomerService, which is also required for a good DIP implementation.
此外,CustomerDao的抽象水平接近于CustomerService,这也是一个好的DIP实现所需要的。
Now, let’s create the low-level component in a different package. In this case, it’s just a basic CustomerDao implementation:
现在,让我们在一个不同的包中创建底层组件。在这种情况下,它只是一个基本的CustomerDao实现。
public class SimpleCustomerDao implements CustomerDao {
// standard constructor / getter
@Override
public Optional<Customer> findById(int id) {
return Optional.ofNullable(customers.get(id));
}
@Override
public List<Customer> findAll() {
return new ArrayList<>(customers.values());
}
}
Finally, let’s create a unit test to check the CustomerService class’ functionality:
最后,让我们创建一个单元测试来检查CustomerService类的功能。
@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);
}
@Test
public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() {
assertThat(customerService.findAll()).isInstanceOf(List.class);
}
@Test
public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() {
var customers = new HashMap<Integer, Customer>();
customers.put(1, null);
customerService = new CustomerService(new SimpleCustomerDao(customers));
Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer"));
assertThat(customer.getName()).isEqualTo("Non-existing customer");
}
The unit test exercises the CustomerService API. And, it also shows how to manually inject the abstraction into the high-level component. In most cases, we’d use some kind of DI container or framework to accomplish this.
该单元测试锻炼了CustomerService API。而且,它还展示了如何手动将抽象注入到高层组件中。在大多数情况下,我们会使用某种DI容器或框架来完成这个任务。
Additionally, the following diagram shows the structure of our demo application, from a high-level to a low-level package perspective:
此外,下图显示了我们的演示应用程序的结构,从高层到低层包的角度。
4.2. Alternative DIP Implementation
4.2.替代性DIP实施
As we discussed before, it’s possible to use an alternative DIP implementation, where we place the high-level components, the abstractions, and the low-level ones in different packages.
正如我们之前所讨论的,可以使用另一种DIP实现方式,即我们将高层组件、抽象和低层组件放在不同的包中。
For obvious reasons, this variant is more flexible, yields better encapsulation of the components, and makes it easier to replace the low-level components.
由于显而易见的原因,这种变体更加灵活,产生了更好的组件封装,并且更容易替换低级别的组件。
Of course, implementing this variant of the pattern boils down to just placing CustomerService, MapCustomerDao, and CustomerDao in separate packages.
当然,实现这种模式的变体可以归结为将CustomerService、MapCustomerDao和CustomerDao放在不同的包中。
Therefore, a diagram is sufficient for showing how each component is laid out with this implementation:
因此,一张图就足以显示每个组件是如何在这个实现中布置的。
5. Java 11 Modular Implementation
5.Java 11的模块化实现
It’s fairly easy to refactor our demo application into a modular one.
将我们的演示程序重构为一个模块化的程序是相当容易的。
This is a really nice way to demonstrate how the JPMS enforces best programming practices, including strong encapsulation, abstraction, and component reuse through the DIP.
这是一个非常好的方式来展示JPMS如何执行最佳的编程实践,包括强封装、抽象和通过DIP的组件重用。
We don’t need to reimplement our sample components from scratch. Hence, modularizing our sample application is just a matter of placing each component file in a separate module, along with the corresponding module descriptor.
我们不需要从头开始重新实现我们的示例组件。因此,模块化我们的示例应用程序只是将每个组件文件与相应的模块描述符一起放在一个单独的模块中而已。
Here’s how the modular project structure will look:
下面是模块化项目结构的样子。
project base directory (could be anything, like dipmodular)
|- com.baeldung.dip.services
module-info.java
|- com
|- baeldung
|- dip
|- services
CustomerService.java
|- com.baeldung.dip.daos
module-info.java
|- com
|- baeldung
|- dip
|- daos
CustomerDao.java
|- com.baeldung.dip.daoimplementations
module-info.java
|- com
|- baeldung
|- dip
|- daoimplementations
SimpleCustomerDao.java
|- com.baeldung.dip.entities
module-info.java
|- com
|- baeldung
|- dip
|- entities
Customer.java
|- com.baeldung.dip.mainapp
module-info.java
|- com
|- baeldung
|- dip
|- mainapp
MainApplication.java
5.1. The High-Level Component Module
5.1.高级组件模块
Let’s start by placing the CustomerService class in its own module.
让我们先把CustomerService类放在自己的模块中。
We’ll create this module in the root directory com.baeldung.dip.services, and add the module descriptor, module-info.java:
我们将在根目录com.baeldung.dip.services>中创建这个模块,并添加模块描述符,module-info.java。
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;
}
For obvious reasons, we won’t go into the details on how the JPMS works. Even so, it’s clear to see the module dependencies just by looking at the requires directives.
由于显而易见的原因,我们不会去讨论JPMS的工作细节。即便如此,只要看看 requires指令,就可以清楚地看到模块的依赖性。
The most relevant detail worth noting here is the uses directive. It states that the module is a client module that consumes an implementation of the CustomerDao interface.
这里最值得注意的相关细节是 uses指令。它指出该模块是一个客户模块,它消耗CustomerDao接口的实现。
Of course, we still need to place the high-level component, the CustomerService class, in this module. So, within the root directory com.baeldung.dip.services, let’s create the following package-like directory structure: com/baeldung/dip/services.
当然,我们仍然需要把高级组件,CustomerService类,放在这个模块中。因此,在根目录com.baeldung.dip.services中,让我们创建以下类似包的目录结构。com/baeldung/dip/services.。
Finally, let’s place the CustomerService.java file in that directory.
最后,让我们把CustomerService.java文件放在该目录中。
5.2. The Abstraction Module
5.2.抽象模块
Likewise, we need to place the CustomerDao interface in its own module. Therefore, let’s create the module in the root directory com.baeldung.dip.daos, and add the module descriptor:
同样地,我们需要将CustomerDao接口放在自己的模块中。因此,让我们在根目录com.baeldung.dip.daos中创建该模块,并添加模块描述符。
module com.baeldung.dip.daos {
requires com.baeldung.dip.entities;
exports com.baeldung.dip.daos;
}
Now, let’s navigate to the com.baeldung.dip.daos directory and create the following directory structure: com/baeldung/dip/daos. Let’s place the CustomerDao.java file in that directory.
现在,让我们导航到com.baeldung.dip.daos目录,创建以下目录结构。com/baeldung/dip/daos。让我们把CustomerDao.java文件放在该目录中。
5.3. The Low-Level Component Module
5.3.低层组件模块
Logically, we need to put the low-level component, SimpleCustomerDao, in a separate module, too. As expected, the process looks very similar to what we just did with the other modules.
从逻辑上讲,我们也需要把底层组件SimpleCustomerDao放在一个单独的模块中。正如预期的那样,这个过程看起来与我们刚才对其他模块所做的非常相似。
Let’s create the new module in the root directory com.baeldung.dip.daoimplementations, and include the module descriptor:
让我们在根目录com.baeldung.dip.daoimplementations中创建新模块,并包含模块描述符。
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;
}
In a JPMS context, this is a service provider module, since it declares the provides and with directives.
在JPMS环境中,这是一个服务提供者模块,因为它声明了provides和with指令。
In this case, the module makes the CustomerDao service available to one or more consumer modules, through the SimpleCustomerDao implementation.
在这种情况下,该模块通过SimpleCustomerDao实现,使CustomerDao服务对一个或多个消费者模块可用。
Let’s keep in mind that our consumer module, com.baeldung.dip.services, consumes this service through the uses directive.
让我们记住,我们的消费者模块,com.baeldung.dip.services,通过uses指令来消费这个服务。
This clearly shows how simple it is to have a direct DIP implementation with the JPMS, by just defining consumers, service providers, and abstractions in different modules.
这清楚地表明只需在不同的模块中定义消费者、服务提供者和抽象,就可以用JPMS直接实现DIP,这是多么简单啊。
Likewise, we need to place the SimpleCustomerDao.java file in this new module. Let’s navigate to the com.baeldung.dip.daoimplementations directory, and create a new package-like directory structure with this name: com/baeldung/dip/daoimplementations.
同样地,我们需要把SimpleCustomerDao.java文件放在这个新模块中。让我们导航到com.baeldung.dip.daoimplementations目录,并创建一个新的类似于包的目录结构,名字是这样的。com/baeldung/dip/daoimplementations。
Finally, let’s place the SimpleCustomerDao.java file in the directory.
最后,让我们把SimpleCustomerDao.java文件放在该目录中。
5.4. The Entity Module
5.4.实体模块
Additionally, we have to create another module where we can place the Customer.java class. As we did before, let’s create the root directory com.baeldung.dip.entities and include the module descriptor:
此外,我们必须创建另一个模块,在那里我们可以放置Customer.java类。正如我们之前所做的,让我们创建根目录com.baeldung.dip.entities并包含模块描述符。
module com.baeldung.dip.entities {
exports com.baeldung.dip.entities;
}
In the package’s root directory, let’s create the directory com/baeldung/dip/entities and add the following Customer.java file:
在包的根目录下,让我们创建目录com/baeldung/dip/entities并添加以下Customer.java文件。
public class Customer {
private final String name;
// standard constructor / getter / toString
}
5.5. The Main Application Module
5.5.主要应用模块
Next, we need to create an additional module that allows us to define our demo application’s entry point. Therefore, let’s create another root directory com.baeldung.dip.mainapp and place in it the module descriptor:
接下来,我们需要创建一个额外的模块,允许我们定义我们的演示应用程序的入口点。因此,让我们创建另一个根目录com.baeldung.dip.mainapp并在其中放置模块描述符。
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;
}
Now, let’s navigate to the module’s root directory, and create the following directory structure: com/baeldung/dip/mainapp. In that directory, let’s add a MainApplication.java file, which simply implements a main() method:
现在,让我们导航到模块的根目录,并创建以下目录结构。com/baeldung/dip/mainapp. 在该目录中,让我们添加一个MainApplication.java文件,它只是实现了一个main()方法。
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);
}
}
Finally, let’s compile and run the demo application — either from within our IDE or from a command console.
最后,让我们编译并运行演示程序–可以从我们的IDE内或从命令控制台。
As expected, we should see a list of Customer objects printed out to the console when the application starts up:
正如预期的那样,当应用程序启动时,我们应该看到一个Customer对象的列表打印到控制台。
Customer{name=John}
Customer{name=Susan}
In addition, the following diagram shows the dependencies of each module of the application:
此外,下图显示了应用程序的每个模块的依赖关系。
6. Conclusion
6.结论
In this tutorial, we took a deep dive into the DIP’s key concepts, and we also showed different implementations of the pattern in Java 8 and Java 11, with the latter using the JPMS.
在本教程中,我们深入探讨了DIP的关键概念,我们还展示了该模式在Java 8和Java 11中的不同实现,后者使用了JPMS。
All the examples for the Java 8 DIP implementation and the Java 11 implementation are available over on GitHub.
Java 8 DIP实现和Java 11实现的所有示例都可以在GitHub上找到。