当涉及依赖注入(Dependency Injection,DI)时,首先推荐使用构造函数注入,因为构造函数注入有很多技术优点,而且还与面向对象的设计原则密切相关。在业界,构造函数注入作为依赖注入的一种最佳实践得到了广泛的认可,在Spring Framework的作者之一Rod Johnson的观点中也得有体现。
下面是Spring官方文档中对于依赖注入的描述:
Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.
The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.
Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of t hat class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection. ——Spring官网原文链接 https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
在本文中,我们将更深入地探讨为何构造函数注入被认为是最佳实践,并将通过详细的Java代码示例来阐明其优点。同时,我们将研究如何将构造函数注入与面向对象的设计理念相结合,特别是如何确保封装、单一责任、不变性和依赖倒置原则得以遵循。
依赖注入是一种关键的技术,可以提高应用程序的可测试性和可维护性。Rod Johnson在他的书中明确指出,通过将依赖项注入到对象中,可以更轻松地进行单元测试,同时降低了对象之间的耦合度。这正是构造函数注入所实现的。当我们在对象的构造函数中传递依赖项时,我们不仅提供了明确的依赖关系,还提高了代码的清晰度。让我们通过一个示例来看看构造函数注入的工作方式。
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// ...
}
在上述示例中,OrderService
的构造函数接受一个OrderRepository
作为参数,明确指定了其依赖关系。这不仅提高了代码的可读性,还使得单元测试变得更加容易。您可以轻松地创建一个模拟的OrderRepository
并将其传递给OrderService
的构造函数,以进行单元测试。
面向对象编程强调封装特性,即将数据和行为封装在类的内部,通过公共接口来访问对象。构造函数注入有助于维护封装特性,因为它允许您在对象内部设置依赖项,而不需要向外部暴露setter方法。这符合依赖倒置原则和接口隔离原则的思想。
通过将依赖项作为构造函数参数传递,您确保了依赖项在对象内部得到了封装。这意味着外部代码无法直接修改对象的依赖项,从而提高了代码的安全性和稳定性。让我们来看一个例子:
public class CustomerService {
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public Customer getCustomerById(int customerId) {
return customerRepository.findById(customerId);
}
// ...
}
在上面的示例中,CustomerService
依赖于CustomerRepository
,并通过构造函数注入的方式获得了该依赖。这确保了customerRepository
的封装性,不允许外部代码直接访问或修改它。
单一责任原则是面向对象设计的基本原则之一,强调一个类应该只有一个理由去改变。构造函数注入有助于实现这一原则,因为它鼓励每个类专注于执行单一任务,而不负责创建或管理依赖项。通过使用构造函数注入,您可以将依赖项的创建和配置从类中分离出来,使每个类专注于自身的主要职责。这提高了代码的模块化性和可维护性。以下是一个示例:
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List getAllProducts() {
return productRepository.getAll();
}
// ...
}
在上述示例中,ProductService
专注于处理产品相关的业务逻辑,而不需要关心如何创建或配置ProductRepository
。这遵循了单一责任原则,使代码更加清晰和可维护。
构造函数注入还有助于防止不必要的可变性,因为一旦依赖项被设置,它们通常是不可变的。不可变对象在面向对象设计中具有重要地位,因为它们更容易理解和维护。通过将依赖项注入到对象中并在构造函数中进行初始化,您可以确保依赖项在对象的整个生命周期内保持不变。这有助于减少对象状态的变化,从而提高了代码的可维护性和可预测性。
构造函数注入与依赖注入容器(如Spring容器)协同工作得很好。您可以使用构造函数注入来定义组件的依赖关系,并让容器负责创建和管理对象的生命周期。
@Component
public class AppConfig {
@Bean
public OrderRepository orderRepository() {
return new JpaOrderRepository();
}
@Bean
public OrderService orderService(OrderRepository orderRepository) {
return new OrderService(orderRepository);
}
}
在上述示例中,我们使用Spring的Java配置来定义OrderRepository
和OrderService
之间的依赖关系,并通过构造函数注入实现了依赖解析。
构造函数注入使得编写单元测试变得更容易,因为您可以轻松地传递模拟或测试用的依赖项到对象的构造函数中。这样,您可以在不依赖于容器或其他复杂配置的情况下,对类进行单元测试。
public class OrderServiceTest {
@Test
public void testCalculateTotalPrice() {
OrderRepository mockRepository = mock(OrderRepository.class);
when(mockRepository.findOrderById(1)).thenReturn(new Order(1, 100.0));
OrderService orderService = new OrderService(mockRepository);
double totalPrice = orderService.calculateTotalPrice(1);
assertEquals(100.0, totalPrice, 0.01);
}
}
上述单元测试中,我们使用构造函数注入创建了一个OrderService
的实例,并注入了一个Mock的OrderRepository
。
通过以上示例,阐述了构造函数注入在依赖注入中的价值,以及它如何与面向对象的设计原则协同工作。这不仅提高了代码的可维护性和可测试性,还使其更符合面向对象设计的最佳实践。构造函数注入作为一种强大的工具,有助于构建高质量、可维护和可测试的应用程序。希望通过本文,您能更深入地了解构造函数注入的价值和实践。
参考
https://www.baeldung.com/constructor-injection-in-spring
https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html
作者:京东物流 张涛
来源:京东云开发者社区 自猿其说Tech 转载请注明来源