DIP và dependency injection
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
OrderService tự tạo SqlOrderRepository bên trong. Muốn chạy test thì phải
có database thật, muốn đổi sang lưu file thì phải sửa OrderService. DIP là
nguyên tắc cuối của SOLID, còn dependency injection là cách làm phổ biến nhất
để đạt được nó.
Khái niệm
🔌 DIP (Dependency Inversion Principle): code nghiệp vụ và class lo database, email không phụ thuộc trực tiếp vào nhau, mà cùng phụ thuộc vào một interface.
💉 Dependency injection (DI): class nhận những thứ nó cần qua constructor, thay vì tự new bên trong.
Ví dụ
// Main.java
void main() {
var repo = new SqlOrderRepository();
var service = new OrderService(repo);
service.place("Bút bi");
}
// OrderRepository.java
interface OrderRepository {
void save(String item);
}
// SqlOrderRepository.java
class SqlOrderRepository implements OrderRepository {
@Override
public void save(String item) {
System.out.println("SQL: lưu " + item);
}
}
// OrderService.java
class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = repo;
}
public void place(String item) {
repo.save(item);
}
}OrderServicechỉ biếtOrderRepository, không biết có SQL. Đó là DIP.OrderServicenhận repository qua constructor. Đó là DI.OrderServiceở bài Interface đã làm đúng như vậy.- Nơi tạo
OrderServicequyết định dùng repository nào.
Thử ngay
Giữ các file của ví dụ. Tạo thêm file dưới đây, rồi đổi
new SqlOrderRepository() ở dòng đầu của main thành
new FakeOrderRepository():
// FakeOrderRepository.java
class FakeOrderRepository implements OrderRepository {
@Override
public void save(String item) {
System.out.println("Giả lập: lưu " + item);
}
}Đoán trước khi chạy: chương trình in ra dòng nào?
Xem kết quả
Giả lập: lưu Bút biOrderService gọi save của object được truyền vào, nên in dòng của
FakeOrderRepository. Viết test không cần database thật cũng theo cách này:
truyền vào một repository giả.
Lỗi hay gặp
Tự new bên trong class. OrderService bị khoá cứng vào SQL.
// SAI — không đổi được, không test được
// OrderService.java
class OrderService {
private final SqlOrderRepository repo =
new SqlOrderRepository();
public void place(String item) {
repo.save(item);
}
}Cách đúng là nhận interface qua constructor, như OrderService ở phần Ví dụ.
Nhận class cụ thể qua constructor. Có DI nhưng vẫn phụ thuộc vào SQL, nên chưa đạt DIP.
// SAI — vẫn chỉ nhận được SqlOrderRepository
// OrderService.java
class OrderService {
private final SqlOrderRepository repo;
public OrderService(SqlOrderRepository repo) {
this.repo = repo;
}
}// ĐÚNG — kiểu tham số là interface
// OrderService.java
class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}Tóm tắt
- DIP: code nghiệp vụ phụ thuộc vào interface, không phụ thuộc vào class cụ thể.
- DI: nhận phụ thuộc qua constructor, không tự
newbên trong. - Nhờ DI, đổi cách làm hay viết test chỉ cần truyền object khác vào.
- Constructor nên nhận interface, không nhận class cụ thể.
Tự kiểm tra
0/3 câuclass ReportService { private final PdfExporter pdf = new PdfExporter(); } Vấn đề chính là gì?
PaymentGateway là interface, VnPayGateway là một class implement nó. Constructor nào đúng tinh thần DIP?
Lợi ích lớn nhất của DI khi viết test là gì?