Xin ví dụ về Dependency injection (DI)

Xin chào, mình đọc định nghĩa về Dependency injection nhưng vẫn cảm thấy khó hiểu, nhờ các bác cho mình xin vài ví dụ thực tế với! thanks

Đây là một ví dụ:

Class A {
   private Datasource datasource;
   Public A(Datasource datasource) {
        this.datasource = datasource
     }
}

Về bản chất: DI nó biểu hiển cho việc một object phụ thuộc vào objec khác. hết. Còn ai tạo ra đối tượng datasource thì nó liên quan đến vấn đề khác(IOC).
Khi em tạo một đối tượng có mối quan hệ has-A thì nó cũng xem như là DI rồi.
Còn em coi trên mạng chắc Ià DI Design Pattern, nó kết hợp giữa DI với DIP( Dependency Inversion Principle) và thường có thêm IOC nữa nên rối là phải.
Em nên tìm hiểu DIP, DI,IOC cùng lúc chắc sẽ nắm rõ hơn về mấy ví dụ trên mạng.
DI,DIP,IOC mỗi trang lại giải thích mỗi kiểu, tốt nhất là theo cây đa cây đề trong ngành thôi.

13 Likes

DI nó là thuật ngữ chung dành cho mọi ngôn ngữ lập trình oop hả anh, vì em làm php magento, lúc interview cũng đc hỏi DI là gì nhưng em cũng không rõ lắm và ko trả lời được, không biết DI trong magento hay là DI trong php hay DI gì nữa. mặc dù em thao tác với file di.xml (trong docs của magento nói di thì các định nghĩa xml trong file này ) của magento khá nhiều

Hầu hết là vậy. Nhưng DI Pattern, không phải là cái hướng đến khi làm oop. nó là một kĩ thuật apply cho cái nguyên tắc (Dependency Inversion Principle)
Khi thiết kế app/webapp, người ta thường có 1 chuẩn/ hay một nguyên tắc và ở đây là DIP.
DIP dịch tiếng Việt nôn na là “đảo ngược sự phụ thuộc”,tại sao lại nói như vậy:
E đọc thấy một số trang nó sẽ nói thế này:

  • Các module, class cấp cao (high-level) không nên phụ thuộc vào module, class cấp thấp hơn (low-level) mà nên phụ thuộc (giao tiếp) với nhau thông qua một abstraction (Interface).
  • Abtraction không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abtraction .

Nghe nó chả ăn nhâp gi với cái từ inversion(đảo ngược cả).
Xem ví dụ bên dưới:


Đây là luôn lập trình bình thường thằng A đang phụ thuộc vào SQLServerDatasouce. e chú ý cái chiều mũi tên nhé. Làm kiều này khi thang SQLserverDatasouce thay đổi, đôi hàm gì đó thăng A cũng phải change theo.
Giờ Apply DIP.

Giờ en thấy cái chiều mũi tên nó thay đổi chưa, giờ 2 thằng nó phụ thuộc khác. Và bởi vi 2 thăng này điều phụ thuộc vào thứ 3 mới sinh ra cho nên thằng thứ 3 nó phải trở nên abstraction hơn. Nó như cái chuẩn mà các thằng phụ thuộc vào phải tuân theo.

class A{
   Datasource ds;
   public A( Datasource ds) {
       this.ds = ds;
   }
}

class Test {

    test () {
          Datasoure sqlDs = new SqlServerDataSourse();
          Datasoure oracleDs = new OracleServerDataSourse();
          A  connect =  new A(sqlDs);
          connect = new A(oracleDs);
    }

}

Làm như vậy hệ thống trở nên " LOW COUPLING" hơn rất nhiều
(ref: https://edwardthienhoang.wordpress.com/2018/01/08/low-coupling-and-high-cohesion/)

Này nó apply IOC rồi. IOC nó liên quan đến việc ai sẽ tạo đối tượng này. nếu code em tạo thì nó không phải là IOC. quăng cho thằng khác tạo dùm thì nó là IOC. ở đây là fw tạo, khi em khai báo trong xml thằng FW nó sẽ quét qua hết toàn bộ và khởi tạo đối tượng dùng em.
Các FW thường sẽ có cả 3 bộ DIP/IOC/DI nhé.

10 Likes

thank a, de em tim hieu them :smiley:

Cậu có thể tham khảo bài trả lời rất xuất sắc của @nguyenhuuca ở trên. Bài viết dưới này chỉ có tác dụng bổ sung thêm thông tin, để cậu hiểu được thực sự di.xml file làm gì.

Nó là concept chung trong lập trình, chỉ kỹ thuật cậu nhét (tiêm - inject) một phụ thuộc (một property của một class) vào trong một object.

Giả sử cậu có:

public class SomeService {
  private SomeDatabaseCallObject dbCaller;
  // The rest of the implementation goes here
}

Mọi object của class SomeService đều phụ thuộc vào việc tồn tại một SomeDatabaseCallObject trong nó. Đó là phụ thuộc - dependency.
Câu hỏi đặt ra là làm thế nào cậu tạo ra SomeDatabaseCallObject object, và “tiêm” vào SomeService?

Cách 1: Cậu có thể khởi tạo dbCaller trong SomeService.

public class SomeService {
  private SomeDatabaseCallObject dbCaller = new SomeDatabaseCallObject();
  // The rest of the implementation goes here
}

Cơ mà cậu sẽ gặp vấn đề:

  • SomeService không thực sự cần biết thể hiện thực sự của dbCaller là gì. Nó chỉ cần biết dbCaller có thể làm được điều nó cần không.
  • Nếu SomeService biết thể hiện thực sự của dbCaller, cậu có SomeService phụ thuộc chặt chẽ vào dbCaller. Cậu sẽ gặp vấn đề nếu như:
    • Cậu cần test SomeService mà không muốn gọi tới DB thật. Vì cậu đã phụ thuộc chặt chẽ vào thể hiện của dbCaller, rất khó để cậu loại bỏ nó.
    • Cậu muốn đổi thể hiện mới của dbCaller. Cậu buộc phải “tiêm” nó thông qua 1 method nào đó, như setter chẳng hạn.

Cách này tuy đơn giản, nhưng cậu sẽ có một chút technical debt ở đây. Điều này sẽ khiến việc maintain code của cậu (trong TH ai đó thực sự cần đổi cách sử dụng DB) khó khăn hơn.

Cách 2: “Tiêm” phụ thuộc thông qua setter.
Lúc đó, cậu sẽ có:

public class SomeService {
  private SomeDatabaseCallObject dbCaller;
  
  public void setDbCaller(SomeDatabaseCallObject dbCaller) {
    this.dbCaller = dbCaller;
  }
  // The rest of the implementation goes here
}

Cậu giải quyết được hoàn toàn các vấn đề của cách đầu tiên, nhưng cậu sẽ gặp một vài vấn đề mới:

  • Cậu luôn phải nhớ việc setDbCaller trước khi làm bất cứ điều gì. Nếu cậu quên, cậu sẽ nhận được NullPointerException.
  • Ai cũng có thể sửa lại dbCaller. Sẽ thế nào khi cậu đang kỳ vọng implement SomeDatabaseCallObject sẽ chạy, nhưng cậu thấy SomeOtherDatabaseCallObject code được thực thi do ai đó sửa lại dbCaller giữa chừng?
    Nó cũng đồng nghĩa với việc cả thế giới biết mối quan hệ giữa 2 class đó, mà thông thường, cậu nên muốn giữ nó đóng gói nhất có thể.

Cách 3: “Tiêm” phụ thuộc thông qua constructor
Lúc đó, cậu sẽ có:

public class SomeService {
  private SomeDatabaseCallObject dbCaller;
  
  public SomeService(SomeDatabaseCallObject dbCaller) {
    this.dbCaller = dbCaller;
  }
  // The rest of the implementation goes here
}

Bằng cách này, hiển nhiên cậu giải quyết được các vấn đề tồn tại ở cách 1 và cách 2, khi mà SomeService luôn có sẵn dbCaller lúc khởi tạo, cùng với sự loosely coupling giữa 2 class (SomeService không cần biết dbCaller được tạo ra như thế nào, chỉ cần biết nó có làm được việc gì thôi).
Nghe hoàn hảo rồi phải không? :smile: Không hẳn đâu. Thử tưởng tượng lúc nào cậu cũng phải maintain toàn bộ việc “tiêm” phụ thuộc ở tất cả các constructor như thế này:

class SomeController {
  private SomeService service;
  public SomeController(SomeService service) {
    this.service = service;
  }
}
// Create Controller:
SomeController controller = new SomeController(new SomeService(new SomeDatabaseCallObject()));

Mệt đúng không? :smile: Vậy nên chúng ta có cách số 4.

Cách 4: Dùng reflection để “tiêm” phụ thuộc.
Nếu cậu chưa biết reflection là gì, cậu nên tìm hiểu thêm.
Nhìn chung, cậu hoàn toàn có thể dùng reflection để:

  • Tạo ra object từ tên class.
  • Gán thuộc tính private cho một object mà chỉ cần biết kiểu của thuộc tính.

Cậu có thể tự tạo ra object của class phụ thuộc từ tên class đó, và “tiêm” phụ thuộc này vào class khác cũng chỉ cần tên của class. Cậu không cần tạo ra 1 constructor hay một setter để tiêm.
Oh, đúng rồi, cách này phá hỏng hết toàn bộ những gì cậu biết về Encapsulation. Cơ mà, cách này sẽ vô cùng flexible, và trong TH này, benefit mà nó mang lại vô cùng lớn tới mức cậu có thể lờ tạm đi những gì cậu biết về Encapsulation.

Vậy nên, tất cả những gì cậu cần là một file mô tả quan hệ giữa các class. Trong ví dụ của chúng ta, cậu chỉ cần mô tả:

  • SomeController sẽ bị phụ thuộc vào SomeService
  • SomeService sẽ bị phụ thuộc vào SomeDatabaseCallObject

Đó là file di.xml của cậu đó :smile:

Tất cả những gì cậu cần là viết một chương trình, đọc file mô tả kia, dùng reflection để khởi tạo tất cả các object cần thiết từ file mô tả đó.
Rất may là chương trình kia thường được framework viết luôn cho cậu rồi, nên tất cả những gì cậu cần làm là viết file mô tả kia, cùng với tạo các class liên quan. Mọi thứ sẽ được tự động ghép nối cho cậu. Reference của những object kia được lưu trong một thứ gọi là “DI Container” (hoặc IOC container, đôi khi các concept đó có thể thay thế cho nhau), và cậu sẽ lấy các object service, controller, etc,… từ DI Container đó.
Một số framework không sử dụng file mô tả kia để đọc, mà dùng annotation để đánh dấu (tag) các class (Spring là một ví dụ, dù nó cũng support cả file mô tả). Tuy nhiên, cơ chế vẫn như nhau, chỉ khác việc họ dùng reflection để đọc thông tin của class mà các annotation đánh dấu, chứ không đọc file mô tả.
Các framework cũng sẽ “đóng gói” toàn bộ logic tạo và “tiêm” các phụ thuộc, nên ít nhất, chỉ có trong logic đó cậu mới “hơi” phá luật, dùng reflection để nhìn hết tất cả các thông tin từ tất cả các class khác.

Hi vọng cậu sẽ có thêm hiểu biết sau khi đọc thêm bài viết bổ sung này.

10 Likes

Anh có thể giải thích lại giúp em đoạn này được không? “Ai đó sửa lại dbCaller giữa chừng” Ai là ai, là cái gì ạ? lỗi xảy ra trong lúc runtime hay lúc compile? do dev code sai hay user làm xảy ra lỗi?

“Ai đó sửa lại dbCaller giữa chừng” Ai là ai, là cái gì ạ?

Ohm, đoạn đó tớ muốn nói tới vấn đề khi inject phụ thuộc qua setter.
Khi cậu offer setter, cậu cũng hiểu là bất cứ ai contribute vào source code cũng có thể set lại giá trị thông qua setter đó, tại bất cứ thời điểm nào, và bất cứ đâu.

Tớ có thể đưa cậu một ví dụ: giả sử khi khởi tạo chương trình, cậu tạo một instance gọi tới MySQL, gán vào biến dbCaller kể trên.
Sau 1 thời gian, để phục vụ cho một tính năng mới, có 1 kỹ sư khác tạo một instance dbCaller mới gọi tới H2DB để lấy một thông tin cache sẵn. Vì model dữ liệu giống nhau, anh chàng này sử dụng trick swap dbCaller qua setter sang instance mới, gọi xong xuôi thì swap back.
Mọi thứ vẫn ổn cho tới khi H2DB bị crash, code gọi tới database tung ra runtime exception, và code swap back dbCaller về MySQL không được thực thi. Lúc này, cậu có một lỗi nghiêm trọng xảy ra ở runtime, mà không ai hiểu tại sao không thể lấy dữ liệu từ MySQL. Ngoài ra, lỗi kiểu này rất khó có thể track được, vì không ai kỳ vọng có sự thay đổi phụ thuộc giữa chừng ở runtime. Tất cả chỉ vì cậu offer setter để inject phụ thuộc và mọi người missused setter đó.

lỗi xảy ra trong lúc runtime hay lúc compile? do dev code sai hay user làm xảy ra lỗi?

Cậu tự trả lời được rồi nha :smile:

Lỗi như tớ đã đề cập ở trên thực tế xuất hiện rất nhiều. Cá nhân tớ từng gặp vấn đề tương tự trong quá khứ, và đã phải rất vất vả để track ra vấn đề.
Vậy nên, nếu tránh được, cậu không nên sử dụng setter để inject phụ thuộc nói riêng, và trong hầu hết mọi TH nói chung.

5 Likes

Cảm ơn anh đã giải thích chi tiết, cho em hỏi thêm 1 vấn đề nữa. Ở post này cụ thể là đoạn này:

Đúng như cái tên “tiêm phụ thuộc”, thường thì sự phụ thuộc này được xác định rõ ràng lúc khởi tạo ứng dụng/object, và chỉ được “tiêm” 1 lần. Cậu thường không muốn switch giữa các phiên bản cài đặt khác nhau của module bị phụ thuộc ở runtime đâu :smile:

Em xin lấy ví dụ về IoC container trong spring, theo như cách giải thích của anh ở trên thì thao tác IoC container “tiêm” (inject) một bean vào constructor của một bean khác chỉ diễn ra một lần duy nhất khi deploy project lần đầu lên web server tomcat và giữ nguyên y như vậy cho đến khi shutdown server đúng không anh? Có nghĩa là trong quá trình runtime, app đang chạy bình thường thì không có tiêm (inject) gì nữa hết đúng không?

1 Like

Đúng rồi cậu. Việc tiêm này in general chỉ diễn ra 1 lần thôi.

5 Likes

nguyên lý Inversion of control(IoC) , Dependency Injection (DI) trong thiết kế code phần mềm. Mình lấy ví dụ ngoài đời thực, mình có laptop dell 7552 gì đó, nó có bản Intel CPU và bản AMD CPU cho khách hàng tha hồ lựa chọn. Ta không nên thiết kế cứng một chiếc laptop sẽ đi với cpu nào cả, mà ta nên thiết kế rằng : giữa Laptop và Cpu có một bản hợp đồng. Các cpu nào ký bản hợp đồng này thì đều được làm việc với laptop. Trong mô hình này, quan hệ giữa chúng là (laptop - has a - cpu) (cpu - is member of - laptop). Bản hợp đồng được nhắc ở đây chính là interface. Nhưng mà sao ta phải phân ra (Laptop has a Cpu) để làm gì, cứ viết code hết vào laptop cũng được mà ? tại vì đây là nguyên lý Single responsibility principle mỗi người một việc phần ai nấy làm, Laptop nó có nhiều việc quá (vừa nhận input từ bàn phím, vừa hiển thị cho user, vừa xử lý tính toán) ta chia bớt việc sang cho thằng Cpu làm tính toán

// không nên
class Laptop{
    AmdCpu cpu;
    void calculate(){
        this.cpu.calculate();
    }
}


// nên
interface ICpu{
    void calculate();
}

class AmdCpu implements ICpu{
    @Override
    void calculate(){
        System.out.println("I am Amd cpu, high performance of Graphic and cheaper");
    }
 }
 
 class Laptop{
     ICpu cpu;
     void calculate(){
         this.cpu.calculate();
     }
     public void setCpu(ICpu newCpu){
         this.cpu = newCpu;
     }
  }
  
  class Main {  
  public static void main(String args[]) { 
      Laptop laptop = new Laptop();
      laptop.setCpu(new AmdCpu()); // Ô yeahhh laptop dễ dàng thay đổi cpu
      laptop.setCpu(new IntelCpu()); // Ô yeahhh laptop dễ dàng thay đổi cpu
  } 
}
3 Likes

Thanks anh nha, em đang học spring và em chợt nghĩ đến 2 sự khác nhau sau khi đọc ví dụ này:

  • Dependency injection có ý nghĩa đối với nhà sản xuất laptop. Chỉ cần tạo ra 1 class laptop dell mà có thể sản ra nhiều laptop cùng mã model mà khác loại CPU: 2 laptop cùng model dell 7552 nhưng 1 máy intel, 1 máy AMD. Và 2 chip CPU này được inject vào máy chỉ một lần duy nhất lúc xuất xưởng và nó không thể thay thế trong quá trình sử dụng cho đến lúc tái chế rác thải (trong thực tế không có quán sửa laptop nào thay CPU intel thành AMD cả).
  • Trường hợp 2 là việc inject này do người dùng inject. Lấy lại ví dụ của anh nhưng thay class CPU thành một linh kiện interface khác là ổ cứng. Class laptop chắc chắn không phụ thuộc và class ổ cứng rồi. Nhưng bất cứ ai cũng có thể inject ổ cứng khác hoàn toàn trong lúc sử dụng (HDD, SSD, khác brand, …).

Em nghĩ cả 2 trường hợp inject này đều diễn ra ở runtime đúng không vậy anh chị?

83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?