當涉及依賴注入(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<Product> 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 轉載請註明來源