Dependency Injection là gì, hướng dẫn và ví dụ

Dependency Injection (DI)

Trong tài liệu có nói thế này:

Dependency Injection is a design pattern, …

Dài dòng vãi, bạn có thể hiểu nó là một phương pháp lập trình, là một thiết kế để bạn có được hiệu quả cao hơn khi code. Trước khi phương pháp này ra đời, bạn vẫn code bình thường, nhưng bây giờ có rồi, đi theo nó sẽ giúp ích nhiều hơn cho việc lập trình của bạn, không theo cũng chả sao cả, code vẫn chạy

Vậy để triển khai Dependency Injection thì chúng ta làm gì?

Ví dụ:

public class Girl{
    private Bikini outfit; // Mỗi cô gái sẽ có một bộ bikini khi ra ngoài
    public Girl(){
      outfit = new Bikini(); // Khi bạn tạo ra 1 cô gái, bạn cho cô ta mặc Bikini chẳng hạn
    }
}

Trước hết, qua đoạn code này, bạn sẽ thấy là khi bạn tạo ra một Girl, bạn sẽ tạo ra thêm 1 bộ Bikini đi kèm với cô gái đó. Lúc này, Bikini tồn tại mang ý nghĩa là dependency (phụ thuộc) của Girl.

Khi khởi tạo thuộc tính như này, bạn vô tình tạo ra một điểm thắt nút trong chương trình của mình, giả sử, Girl muốn mặc một bộ thứ khác, chẳng hạn như Váy, Áo thun,… hay không mặc gì thì sao? Thì thằng ngu nó sẽ phải thay Class Bikini thành SkirtWithTshirt hay Naked , chắc chắn rồi 😉

Hay nguy hiểm hơn, class Bikini bị hỏng? nó sẽ ảnh hưởng trực tiếp tới Girl.

Vấn đề là ở đó, nguyên tắc là:

Các Class không nên phụ thuộc vào các kế thừa cấp thấp, mà nên phụ thuộc vào Abstraction (lớp trừu tượng).

Nên bây giờ mình thay đoạn code như này:

// Một interface cho việc ăn mặc
public interface Outfit {
  public void wear();
}

// Một object cấp thấp, implement của Outfits
public class Bikini implements Outfit {
  public void wear() {
    System.out.println("Đã mặc Bikini");
  }
}

// Bây giờ Girl chỉ phụ thuộc vào Outfit. nếu muốn thay đổi đồ của cô gái, chúng ta chỉ cần cho Outfit một thể hiện mới.
public class Girl{
    private Outfit outfit;
    public Girl(){
      outfit = new Bikini();
    }
}

Tới đây, chúng ta mới chỉ Abstract hóa thuộc tính của Girl mà thôi, còn thực tế, Girl vẫn đang bị gắn với một bộ Bikini duy nhất. Vậy muốn thay đồ cho cô gái, bạn phải làm như nào.

Phải sửa code thêm chút nữa:

public class Girl{
    private Outfit outfit;
    public Girl(Outfit anything){
      this.outfit = anything // Tạo ra một cô gái, với một món đồ tùy biến
      // Không bị phụ thuộc quá nhiều vào thời điểm khởi tạo, hay code.
    }
}

public class Main {
  public static void main(String[] args) {
    Outfit bikini = new Bikini(); // Tạo ra đối tượng Bikini ở ngoài đối tượng
    Girl girl = new Girl(bikini); // Mặc nó vào cho cô gái khi tạo ra cô ấy.
  }
}

Bây giờ Girl sẽ hoạt động với Outfit mà thôi. Và Outfit ở đâu ra?

Khái niệm Dependency Injection từ đây mà ra~

Dependency Injection là việc các Object nên phụ thuộc vào các Abstract Class và thể hiện chi tiết của nó sẽ được Inject vào đối tượng lúc runtime.

Bây giờ muốn Girl mặc gì khác, bạn chỉ cần tạo một Class kế thừa Outfit và Inject (dịch là Tiêm vào cũng được) nó vào Girl là xong!

Các cách để Inject dependency vào một đối tượng có thể kể đến như sau:

  • Constructor Injection

    : Cái này chính là ví dụ của mình, không hiểu xem lại, viết nhiều mỏi tay

  • Setter Injection

    : Dùng cái Setter mà bạn đã học từ những bài học vỡ lòng: girl.setOutfit(new Naked())

  • Interface Injection

    : Mỗi Class muốn inject cái gì, thì phải implement một Interface có chứa một hàm inject(xx) (Gần như thay thế cho setter). Rồi bạn muốn inject gì đó thì gọi cái hàm inject(xx) ra. Cách này hơi dài và khó cho người mới, nói chùng là cách này mấy bố rảnh quá ngồi nghĩ ra cho nhìn nó ghê, lỡ như có thằng noob nào hỏi là sủa lên cho oai chứ người thường ngồi nghĩ một chặp cũng tự ra.

Inversion of Control

Dependency Injection giúp chúng ta dễ dàng mở rộng code và giảm sự phụ thuộc giữa các dependency với nhau. Tuy nhiên, lúc này, khi code bạn sẽ phải kiêm thêm nhiệm vụ Inject dependency. Thử tưởng tượng một Class có hàng chục dependency thì bạn sẽ phải tự tay inject từng ý cái. Việc này lại dẫn tới khó khăn trong việc code, quản lý code và dependency

public static void main(String[] args) {
    Outfit bikini = new Bikini();
    Accessories gucci = new GucciAccessories();
    HairStyle hair = new KoreanHairStyle();
    Girl ngocTrinh = new Girl(bikini, gucci, hair);
}

Giá như lúc này có thằng làm hộ được chúng ta việc này thì tốt biết mấy, nhưng đéo.

Bây giờ giả sử, chúng ta định nghĩa trước toàn bộ các dependency có trong Project, mô tả nó và tống nó vào 1 cái kho và giao cho một thằng tên là framework quản lý. Bất kỳ các Class nào khi khởi tạo, nó cần dependency gì, thì cái framework này sẽ tự tìm trong kho rồi inject vào đối tượng thay chúng ta. sẽ tiện hơn phải không?

Khi đó, code chúng ta sẽ chỉ cần như này, để lấy ra 1 đối tượng:

@Override
public void run(String... args) throws Exception {
    Girl girl = context.getBean(Girl.class);
}

Đối với Java thì có một số Framework hỗ trợ chúng ta Inversion of Control (IOC), trong đó nổi bật có:

  • Spring framework

  • Mình biết bây nhiêu mấy

Lợi ích của Dependency Injection

. Giảm sự phụ thuộc giữa các class, các module.

. Giảm sự kết dính giữa các module Rất dễ test và viết Unit Test

. Code dễ bảo trì, dễ thay thế.

. Dễ dàng thấy quan hệ giữa các module (Vì các dependecy đều được inject vào constructor)

Nhược điểm

. Khái niệm DI khá “khó tiêu”, các developer mới sẽ gặp khó khăn khi học

. Sử dụng interface nên đôi khi sẽ khó debug, do không biết chính xác module nào được gọi

. Các object được khởi tạo toàn bộ ngay từ đầu, có thể làm giảm performance

. Làm tăng độ phức tạp của code

Lời kết

Hi vọng các bạn sẽ có được góc nhìn gần gửi, thực tiễn và dễ hiểu.

Chúc các bạn học tốt và nhớ chia sẻ cho bạn học cùng.