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
Xin ví dụ về Dependency injection (DI)
Đâ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.
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é.
thank a, de em tim hieu them
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ủadbCaller
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ủadbCaller
, cậu cóSomeService
phụ thuộc chặt chẽ vàodbCaller
. 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ủadbCaller
, 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ậu cần test
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 implementSomeDatabaseCallObject
sẽ chạy, nhưng cậu thấySomeOtherDatabaseCallObject
code được thực thi do ai đó sửa lạidbCaller
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? 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? 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àoSomeService
-
SomeService
sẽ bị phụ thuộc vàoSomeDatabaseCallObject
Đó là file di.xml của cậu đó
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.
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
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.
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
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?
Đúng rồi cậu. Việc tiêm này in general chỉ diễn ra 1 lần thôi.
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
}
}
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ị?