VieTopik
Tiếng HànTiếng Anh.NET (C#)Java
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
OOP và thiết kế (Java)SOLID
Bài 12/12
5 phút

DIP và dependency injection

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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);
    }
}
  • OrderService chỉ biết OrderRepository, không biết có SQL. Đó là DIP.
  • OrderService nhận repository qua constructor. Đó là DI. OrderService ở bài Interface đã làm đúng như vậy.
  • Nơi tạo OrderService quyết định dùng repository nào.
Trước Sau implement OrderService SqlOrderRepository OrderService OrderRepository SqlOrderRepository
Trước: phụ thuộc thẳng vào SQL. Sau: cả hai hướng về interface

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 bi

OrderService 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ự new bê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âu
Câu 1

class ReportService { private final PdfExporter pdf = new PdfExporter(); } Vấn đề chính là gì?

Câu 2

PaymentGateway là interface, VnPayGateway là một class implement nó. Constructor nào đúng tinh thần DIP?

Câu 3

Lợi ích lớn nhất của DI khi viết test là gì?

ISP — Interface nhỏ, đúng việc

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt