6 Phút Để Hiểu Rõ Về Dependency Injection

Những dự án với độ phức tạp cao ngoài việc thiết kế tính năng cho ứng dụng, tổ chức code luôn luôn là vấn đề được đặt lên hàng đầu. Tổ chức tốt giúp lập trình viên dễ dàng bảo trì, cũng như mở rộng code về sau.

Để có thể tiết kiệm chi phí và thời gian cho công đoạn này nhưng vẫn đem lại hiệu quả cao, việc nắm vững về các design pattern sẽ giúp ích rất nhiều. Dependency Injection là một dạng design pattern được thiết kế với mục đích ngăn chặn sự phụ thuộc giữa các class, để khiến cho code dễ hiểu hơn, trực quan hơn, nhằm phục vụ cho mục đích bảo trì và nâng cấp code.

Tuy nhiên nếu sau khi đọc xong thuật ngữ trên bạn vẫn chưa hiểu gì thì hãy coi đây là chuyện bình thường vì các định nghĩa về một design pattern thường khá trừu tượng (abstract) mà không đi vào cụ thể. Việc đi sử dụng Dependency Injection như thế nào sẽ phụ thuộc vào cách triển khai trong từng tình huống cụ thể (cũng như kỹ năng lập trình của developer).

Vậy Dependency Injection cụ thể là gì ? 

Trước khi đi sâu tìm hiểu, hãy cùng xem qua một số khái niệm.

Theo Wikipedia:

“Trong kỹ thuật phần mềm, dependency injection là một kỹ thuật theo đó một đối tượng (hoặc static method) cung cấp các phụ thuộc của đối tượng khác. Một phụ thuộc là một đối tượng có thể được sử dụng (service).”

Nhưng nó vẫn khá khó hiểu, vậy hãy cũng làm rõ nó.

Đầu tiên, dependency hay dependent nghĩa là phụ thuộc vào hỗ trợ của một cái gì, việc gì đó. Ví dụ như nếu chúng ta phụ thuộc quá nhiều vào smartphone, thì có thể hiểu là chúng ta đã dependent lên smartphone, chúng ta phụ thuộc vào robot, đấy chính là chúng ta đã dependent lên robot.

Trước khi nói về dependency injection, hãy hiểu xem dependency trong lập trình nghĩa là gì trước.

Khi mà class A sử dụng một số chức năng của class class B, thì có thể nói là class A có quan hệ phụ thuộc với class B.

Trong java, trước khi ta có thể sử dụng method của class khác, ta phải khởi tạo một object của class đấy (hay A cần phải tạo 1 thực thể của B). Vậy ta có thể hiểu, việc chuyển giao nhiệm vụ khởi tạo object đó cho một ai khác và trực tiếp sử dụng các dependency đó được gọi là dependency injection.

Hay nói một cách chính xác và khách quan hơn là:

Dependency injection (DI) là một kỹ thuật lập trình giúp tách một class độc lập với các biến phụ thuộc. Với lập trình hướng đối tượng, chúng ta hầu như luôn phải làm việc với rất nhiều class trong một chương trình. Các class được liên kết với nhau theo một mối quan hệ nào đó. Dependency là một loại quan hệ giữa 2 class mà trong đó một class hoạt động độc lập và class còn lại phụ thuộc bởi class kia.

Nếu nó là design pattern, vậy có mấy loại Dependency Injection ? 

Thông thường, chúng ta chỉ thường gặp ba loại Dependency Injection sau:

  • Constructor injection

    : Các dependency (biến phụ thuộc) được cung cấp thông qua constructor (hàm tạo lớp).

  • Setter injection: Các dependency (biến phụ thuộc) sẽ được truyền vào 1 class thông qua các setter method (hàm setter).

  • Interface injection: Dependency sẽ cung cấp một Interface, trong đó có chứa hàm có tên là Inject.  Các client phải triển khai một Interface mà có một setter method dành cho việc nhận dependency và truyền nó vào class thông qua việc gọi hàm 

    Inject

     của Interface đó.

Vậy cụ thể nhiệm vụ của Dependency Injection là:

  1. Tạo ra các object.
  2. Biết được class nào cần những object đấy.
  3. Cung cấp cho những class đó những object chúng cần.

Bên cạnh đó, các bạn cũng nên chú ý tới một số khái niệm tương tự như Dependency Inversion, Inversion of Control (IoC), Dependency Injection (DI). Ba khái niệm này tương tự nhau nhưng không hoàn toàn giống nhau, nếu có thể, bạn nên tìm hiểu từng cái để tránh những lẫn lộn không cần thiết.

IoC là hướng đi, DIP là định hình cụ thể của hướng đi, còn DI là một hiện thực cụ thể.

Tại sao phải dùng Dependency Injection ? Khi nào dùng tới nó ? Thực hiện nó ra sao ?

Dependency Injection có thể được thực hiện dựa trên các quy tắc sau:

  • Các class sẽ không phụ thuộc trực tiếp lẫn nhau mà thay vào đó chúng sẽ liên kết với nhau thông qua một Interface hoặc base class (đối với một số ngôn ngữ không hỗ trợ Interface)
  • Việc khởi tạo các class sẽ do các Interface quản lí thay vì class phụ thuộc nó

Giả sử, chúng ta có một class Car, trong đó có vài object khác như Wheel, hay Battery:

class Car{
  private Wheels wheel = new MRFWheels();
  private Battery battery = new ExcideBattery();
  ...
  ...
}

Ở đây, class Car chịu trách nhiệm khởi tạo tất cả các dependency object. Nhưng chuyện gì sẽ xảy ra nếu chúng ta muốn bỏ MRFWheels và thay thế bằng BMWWheels.

Lúc này chúng ta phải tạo lại đối tượng car mới với phụ thuộc mới (new dependecy) là BMWWheels. Rồi sau này nữa, bạn lại muốn độ bánh xe lên, hay thay bánh khác thì sao??? Mỗi lần vậy thêm một loạt code và khi đó chưa chắc chúng đã chạy được, chưa kể là cực kỳ khó đọc.

Dependency Injection là một dạng design pattern được thiết kế nhằm ngăn chặn sự phụ thuộc nêu trên, khi sử dụng dependency injection, chúng ta có thể đổi wheel ở runtime vì dependency có thể được truyền vào (inject) ở runtime thay vì complile time, điều này giúp giảm chi phí trong việc sửa đổi và nâng cấp hệ thống. Nhờ vậy khi bạn thực thiện thay đổi một class A thì những class chứa biến kiểu class A cũng không cần phải thay đổi theo. 

Bạn có thể hiểu là dependency injection là một người trung gian chịu trách nhiệm tạo ra các loại wheel khác nhau, rồi cung cấp chúng cho class Car. Việc đó làm cho class Car ko phải phụ thuộc vào Wheels cụ thể nào hay Battery cụ thể nào nữa.

class Car{
  private Wheels wheel;
  private Battery battery;
  
  /*Ở đâu đó trong project, ta khởi tạo những objects mà đc yêu cầu bởi class này
    Có 2 cách để implement dependency injection
    1. Dựa vào constructor
    2. Dựa vào Setter method
  */
  
  // Dựa vào constructor
  Car(Wheel wh, Battery bt) {
    this.wh = wh;
    this.bt = bt;
  }
  
  // Dựa vào Setter method
  void setWheel(Batter bt){
    this.bt = bt;
  }
  ...  
  ...
}

Lợi ích và bất cập khi dùng Dependency Injection

Lợi ích khi dùng Dependency Injection:

  • Dễ test và viết Unit Test: Dễ hiểu là khi ta có thể Inject các dependency vào trong một class thì ta cũng dễ dàng “tiêm” các mock object vào class (được test) đó.
  • Dễ dàng thấy quan hệ giữa các object: Dependency Injection sẽ inject các object phụ thuộc vào các interface thành phần của object bị phụ thuộc nên ta dễ dàng thấy được các dependency của một object.
  • Dễ dàng hơn trong việc mở rộng các ứng dụng hay tính năng.
  • Giảm sự kết dính giữa các thành phần, tránh ảnh hưởng quá nhiều khi có thay đổi nào đó.

Bất lợi của Dependency Injection:

  • Nó khá là phức tạp để học, và nếu dùng quá đà thì có thể dẫn tới một số vấn đề khác.
  • Rất nhiều các lỗi ở compile time có thể bị đẩy sang runtime, dẫn đến đ

    ôi khi sẽ khó debug. Vì sử dụng các 

    Interface

     nên có thể gặp khó khăn khi ta debug source code vì không biết implement nào thực sự được truyền vào.

  • Có thể làm ảnh hưởng tới chức năng auto-complete hay find references của một số IDE. Cụ thể v

    ì Dependency Injection ẩn các dependency nên một số lỗi chỉ xảy ra khi chạy chương trình thay vì có thể phát hiện khi biên dịch chương trình.

  • Khó khăn lớn nhất là khi người mới vào làm bằng DI sẽ không hiểu rõ ràng tư tưởng, khiến quá trình làm DI vẫn bị nhập nhằng và các injector bị ràng buộc mà không thoát hẳn ra theo tư tưởng của DI.

Tạm kết

Bên trên mình đã tổng kết lại một số kiến thức về Dependency Injection qua những thông tin mình tìm kiếm được trên mạng, hy vọng bài viết giúp ích được phần nào cho mọi người. Nếu có sai sót, xin hãy để lại bình luận phía bên dưới, mình sẽ cập nhật nhanh nhất có thể.

Sự ủng hộ của mọi người là động lực để mình tiếp tục cho ra những bài viết tiếp theo. Cảm ơn mọi người !