Giới thiệu Inversion of Control và Dependency Injection trong Spring

Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu các khái niệm của IoC (Inversion of Control) và DI (Dependency Injection). Và sau đó chúng ta xem cách Spring framework triển khai các khái niệm này như thế nào nha.

Inversion of Control là gì?

Nhắc lại kiến thức

Trước khi bắt đầu, các bạn cần nhớ lại nguyên tắc Dependency Inversion trong SOLID phát biểu rằng:

Các module cấp cao không nên phụ thuộc vào module cấp thấp, cả hai nên phụ thuộc vào interface.
Các class giao tiếp với nhau thông qua interfce, không phải thông qua implementation

Trong lập trình truyền thống, các module cấp cao sẽ gọi đến các module cấp thấp. Như vậy những module cấp cao sẽ phụ thuộc và các module cấp thấp, điều đó tạo ra các dependency. Khi các module cấp thấp thay đổi thì các module cấp cao phải phải thay đổi theo, như vậy sẽ làm tăng tính phức tạp của code và khó để bảo trì.

Nếu áp dụng Dependency Inversion, các module sẽ giao tiếp với nhau thông qua interface, cả module cấp cao và cấp thấp sẽ không phụ thuộc lẫn nhau mà phụ thuộc vào interface. Ta có thể dễ dàng thay thế các implementation của các module cấp thấp miễn là chúng đều triển khai một interface.

Như vậy IoC là một design pattern được tạo ra nhầm tuân thu nguyên lý Dependency Inversion. Chúng ta có thể triển khai IoC thông qua các cơ chế như Strategy design pattern, Service Locator pattern, Factory pattern, và Dependency Injection (DI). 

Kiến trúc này sẽ mang lại những lợi ích:

  • Tách bạch phần thực thi và phần triển khai của một tác vụ.
  • Dễ dàng chuyển đổi giữa các implementation của cùng một interface.
  • Tăng tính modun hoá cho chương trình.
  • Dễ dàng kiểm thử chương trình bằng cách cô lập một component hoặc giả lập các dependency của nó và cho phép component giao tiếp thông qua các kịch bản dựng sẵn.

Phần tiếp theo chúng ta sẽ tìm hiểu Dependency Injection một trong những cách giúp chúng ta triển khai IoC hiệu quả.

Dependency Injection là gì?

Dependency injection – DI cũng là một design pattern được sử dụng để triển khai IoC, trong đó quyền kiểm soát các phụ thuộc của một object sẽ bị đảo ngược và nó không được quyền quản lý.  Việc kết nối giữa các object được thực hiện bởi một trình quản lý thay vì tự thân chúng kết nối với nhau.

Để hiểu rõ hơn, dưới đây là cách bạn tạo một object 

public class Store {
    private Item item;
 
    public Store() {
        item = new ItemImpl1();    
    }
}

Trong ví dụ trên chúng ta cần khởi tạo một implementation của Item interface bên trong Store class.

Nếu sử dụng DI, chúng ta có thể triển khai Store class lại như sau mà không cần khởi tạo một Item implementation.

public class Store {
    private Item item;
    public Store(Item item) {
        this.item = item;
    }
}

IoC và DI là 2 khái niệm cơ bản giúp mã nguồn của chúng ta trở nên dễ đọc và dễ bảo trì hơn. Chính vì vậy Spring framework đã tích hợp sẵn DI trong đó.

Spring IoC Container 

IoC container là một trong những khái niệm dùng chung cho các framework triển khai Inversion of Control – IoC.

Trong Spring, IoC container được đại diện bởi ApplicationContext interface. Spring container chịu trách nhiệm khởi tạo, cấu hình và lắp ráp các object được biết đến là các Bean trong Spring, nó cũng quản lý luôn vòng đời của chúng.

Spring cung cấp một số cách triển khai ApplicationContext interface như thông qua ClassPathXmlApplicationContext FileSystemXmlApplicationContext cho các ứng dụng độc lập và WebApplicationContext cho các ứng dụng web.

Để lắp ráp các Bean trong ứng dụng, container sẽ sử dụng các metadata được cấu hình ở dạng XML hoặc annotation.

Dưới đây là một cách thủ công để tạo một Spring container

ApplicationContext context
  = new ClassPathXmlApplicationContext("applicationContext.xml");

Để đặt giá trị cho một Item implementation ở trên thì chúng ta có thể chỉ định nó trong metadata. Và sau đó container sẽ đọc thông tin này và sử dụng nó để tạo ra một Bean tương ứng với Item interface.

Dependency Injection trong Spring có thể thực hiện thông qua constructor, setter, fields mà chúng ta sẽ tìm hiểu ở những phần sau.

Constructor-Based Dependency Injection

Trong trường hợp sử dụng constructor-dependency-injection (tiêm các dependency của một class thông qua constructor) thì container sẽ gọi constructor với các tham số đại diện cho các dependency mà chúng ta muốn. 

Như vậy các bạn thấy việc khởi tạo và quản lý các dependency giờ đây không phải do Class sử dụng chúng quản lý mà thuộc về container.

@Configuration
public class AppConfig {

    @Bean
    public Item item1() {
        return new ItemImpl1();
    }

    @Bean
    public Store store() {
        return new Store(item1());
    }
}

@Configuration annotation đại diện cho một class định nghĩa các Bean. @Bean annotation được sử dụng trên các method định nghĩa ra một Bean. Nếu chúng ta không chỉ định tên của Bean thì tên mặc định của nó là tên hàm.

Mỗi Bean chỉ được tồn tại một object trong container. Spring sẽ kiểm tra nếu đã tồn tại một object của một Bean trong container thì nó sẽ không khởi tạo lại, ngược lại một object sẽ được tạo mới và thêm vào container.

Một cách khác để tạo cấu hình của bean là thông qua cấu hình XML:
<bean id="item1" class="org.deft.store.ItemImpl1" /> 
<bean id="store" class="org.deft.store.Store"> 
    <constructor-arg type="ItemImpl1" index="0" name="item" ref="item1" /> 
</bean>

Setter-Based Dependency Injection

Đối với setter-dependency-injection thì các dependency thì được tiêm vào class của chúng ta thông qua setter method.

Ví dụ sau khi khởi tạo Item Bean chúng ta có thể khởi tạo Store với Item Bean đã khởi tạo trước đó.

@Bean
public Store store() {
    Store store = new Store();
    store.setItem(item1());
    return store;
}

Hoặc cấu hình XML như sau:

<bean id="store" class="org.baeldung.store.Store">
    <property name="item" ref="item1" />
</bean>

Field-Based Dependency Injection

Đối với trường hợp sử dụng Field-Based DI, chúng ta sẽ tiêm các dependency của class thông qua @Autowired annotation

public class Store {
    @Autowired
    private Item item; 
}

Trong khi khởi tạo một object Store, nếu không có constructor hoặc setter nào để tiêm Item bean vào, container sẽ sử dụng reflection để đưa Item vào Store.

Cách tiếp cận này có thể trông đơn giản và gọn gàng hơn nhưng không được khuyến khích sử dụng vì nó có một số nhược điểm như:

  • Phương pháp này sử dụng reflection để đưa vào các dependency, tốn kém hơn so với tiêm dựa trên constructor hoặc setter.
  • Nếu sử dụng phương pháp này sẽ khiến bạn cảm thấy dễ dàng khi thêm các phụ thuộc, trong khi với phương pháp sử dụng constructor khi các tham số trở nên nhiều có thể giúp bạn nghĩ đến tình trạng vi phạm Single Responsibility Principle.

Autowiring Dependencies

Autowire cho phép Spring container tự động giải quyết các dependency theo 4 chiến lược sau:

  • no – Giá trị mặc định, nghĩa là không có cơ chế nào của Autowire được cung cấp và bạn phải chỉ định tên của các dependency một cách rõ ràng.
  • byName – Spring container sẽ tìm kiếm một bean với tên trùng với tên của thuộc tính mà chúng ta đặt trong class.
  • byType – Spring container sẽ tìm kiếm một bean cùng kiểu dữ liệu với thuộc tính mà chúng ta đặt trong class. Nếu có nhiều hơn một bean được tìm thấy nó sẽ ném ra một exception.
  • constructor – Spring container sẽ tìm kiếm các dependency dựa vào các tham số trên constructor có cùng kiểu dữ liệu với các thuộc tính khai báo trong class.

Ví dụ autowire item1 bean trong store bean.

@Bean(autowire = Autowire.BY_TYPE)
public class Store {
    
    private Item item;

    public setItem(Item item){
        this.item = item;    
    }
}

Chúng ta cũng có thể làm việc này với @Autowired annotation

public class Store {
    
    @Autowired
    private Item item;
}

Nếu có nhiều hơn một bean cùng loại, chúng ta có thể sử dụng chú thích @Qualifier để tham chiếu đến một bean theo tên:

public class Store {
    
    @Autowired
    @Qualifier("item1")
    private Item item;
}

Lazy Initialized Bean

Mặc định thì Spring container sẽ khởi tạo và cấu hình tất cả các bean tại thời điểm khởi chạy chương trình. Nếu không muốn, bạn có thể chỉ định thuộc tính lazy-init  trên bean mà bạn không muốn tạo ban đầu. 

bean id="item1" class="org.baeldung.store.ItemImpl1" lazy-init="true" />

Như vậy, itemq bean sẽ không được khởi tạo cho đến khi chương trình cần đến nó. Cách này có một điểm tốt là chương trình sẽ khởi chạy nhanh hơn, nhưng bạn phải đánh đổi là các lỗi cấu hình sẽ không được phát hiện tại thời điểm khởi chạy mà phải chờ cho đến khi bean được khởi tạo lần đầu tiên.

Kết bài

Có vẽ những khái niệm trên khá nặng nề, ngay cả những người mới đi làm hoặc làm tầm 1-2 năm cũng rất khó để hiểu. Nên các bạn cũng đừng bở ngỡ nhé, cứ đọc để có khái niệm trước cũng được, từ từ nó ngấm. Bên dưới là các tài liệu mình tham khảo các bạn có thể vào đó để tìm hiểu kỹ hơn.

Nguồn tham khảo

https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/beans.html

Dependency Injection và Inversion of Control – Phần 1: Định nghĩa

https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring#autowiring-dependencies

3 3 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x