← SOLID Principles

Bài 6 · Nâng cao · 22 phút· Cập nhật 11/06/2026

D - Dependency Inversion

Biên soạn bởi Nguyễn Anh Tuấn

Dependency Inversion Principle (DIP): nguyên tắc đảo ngược sự phụ thuộc, các module cấp cao không nên phụ thuộc vào module cấp thấp.

Dependency Inversion Principle (DIP): module cấp cao (nghiệp vụ) không nên phụ thuộc vào module cấp thấp (chi tiết kỹ thuật) - cả hai cùng phụ thuộc vào abstraction. Xem một lớp vi phạm:

❌ Vi phạm DIP - tự new chi tiết cụ thể

class SMSSender {
  send(message: string): void {
    console.log("Gui SMS:", message);
  }
}

class NotificationService {
  private sender = new SMSSender();   // bam chat vao SMS cu the
  notify(message: string): void {
    this.sender.send(message);
  }
}
  • NotificationService (cấp cao) tự tạo SMSSender (cấp thấp) → coupling chặt.
  • Muốn đổi sang Email/Push? Phải mở và sửa chính NotificationService.
  • Khó test: gọi notify() là dính tới SMS thật, không thay được bằng bản giả.

Định nghĩa một abstraction (interface) cho việc gửi; cho các implementation tuân theo; tiêm qua constructor. Đây cũng là cách mở rộng mà không sửa code cũ (OCP): thêm kênh gửi mới chỉ là thêm một lớp implement Sender.

✅ Tuân thủ DIP - đảo chiều phụ thuộc

// Abstraction do tang cao dinh nghia
interface Sender {
  send(message: string): void;
}

// Cac chi tiet cap thap "tuan theo" abstraction
class SmsSender implements Sender {
  send(message: string) { console.log("SMS:", message); }
}
class EmailSender implements Sender {
  send(message: string) { console.log("Email:", message); }
}

class NotificationService {
  // Chi biet Sender (abstraction), khong biet la SMS hay Email
  constructor(private sender: Sender) {}
  notify(message: string) { this.sender.send(message); }
}

// "Diem lap rap" (composition root): quyet dinh dung cai nao
const service = new NotificationService(new EmailSender());
service.notify("Don hang da xac nhan");
  • NotificationService phụ thuộc Sender (abstraction), không phụ thuộc lớp cụ thể.
  • Đổi SMS ↔ Email chỉ thay ở điểm lắp ráp - không sửa nghiệp vụ.
  • Đây là Dependency Injection: phụ thuộc được "tiêm" từ ngoài vào.

notification.test.ts - tiêm bản giả

import { describe, it, expect } from "vitest";

class FakeSender implements Sender {
  sent: string[] = [];
  send(message: string) { this.sent.push(message); }
}

describe("NotificationService", () => {
  it("goi sender voi dung noi dung", () => {
    const fake = new FakeSender();
    new NotificationService(fake).notify("xin chao");
    expect(fake.sent).toEqual(["xin chao"]);
  });
});
  • Tiêm FakeSender → test không gửi SMS/Email thật.
  • Kiểm được hành vi (đã gọi send với gì) mà không chạm dịch vụ ngoài.
  • Code testable ở ranh giới I/O gần như luôn tuân DIP.
  • DIP = nguyên tắc (phụ thuộc abstraction); DI = kỹ thuật (tiêm phụ thuộc từ ngoài).
  • Không cần framework: truyền qua constructor ở composition root là đủ cho dự án vừa.
  • Đặt interface ở ĐÚNG ranh giới hay đổi (I/O, nhà cung cấp ngoài) - đừng bọc interface cho mọi lớp.

Đừng abstraction hoá mọi thứ

DIP đáng giá ở các ranh giới: gửi tin, lưu trữ, gọi API ngoài, thanh toán - nơi implementation hay thay và cần test bằng bản giả. Bọc interface quanh một lớp thuần tuý nội bộ, chỉ-có-một-bản và không cần test thay thế thường chỉ thêm gián tiếp vô ích. Và khi đã có interface, hãy giữ nó nhỏ và đúng nhu cầu theo Interface Segregation (ISP).

Câu hỏi thường gặp

Không. DIP là NGUYÊN TẮC: hãy để code phụ thuộc vào abstraction (interface) thay vì chi tiết. DI là KỸ THUẬT để đạt điều đó: thay vì lớp tự new phụ thuộc, ta "tiêm" phụ thuộc từ ngoài vào (qua constructor/tham số). DI là một cách phổ biến để thực thi DIP.

Xem DIP phối hợp cả 5 nguyên tắc: bài Áp dụng SOLID tổng hợp →

Bình thường module cấp cao (nghiệp vụ) phụ thuộc thẳng vào module cấp thấp (chi tiết: SMS, DB...). DIP đảo lại: cả hai cùng phụ thuộc vào một ABSTRACTION do tầng cao định nghĩa. Chi tiết giờ phải "tuân theo" interface của nghiệp vụ, chứ không phải nghiệp vụ chạy theo chi tiết.

Không bắt buộc. DIP đạt được chỉ bằng cách truyền phụ thuộc qua constructor (DI thủ công). Framework DI chỉ giúp TỰ ĐỘNG lắp ráp khi đồ thị phụ thuộc lớn. Dự án nhỏ thì new + truyền tay ở "điểm lắp ráp" (composition root) là đủ.

Rất nhiều. Vì nghiệp vụ phụ thuộc interface, khi test bạn tiêm một implementation giả (fake/mock) thay cho SMS/DB thật. Test chạy nhanh, không gửi SMS thật, không cần DB. Đây là lý do code "testable" gần như luôn tuân DIP ở các ranh giới I/O.

Thực hành tiêm bản giả: bài Dự án - thiết kế lại theo SOLID →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/5 Điểm: 0

Dependency Inversion Principle nói gì?

Bài tập về nhà

  1. 1

    Tìm "new" cứng

    Tìm trong code một lớp nghiệp vụ tự new một chi tiết cụ thể (SMS, HTTP client, DB...). Chỉ ra đổi nhà cung cấp thì phải sửa ở đâu.

    ✅ Hoàn thành khi: Chỉ ra điểm coupling chặt; mô tả thay đổi sẽ lan tới đâu.

  2. 2

    Đảo chiều bằng interface

    Refactor ví dụ NotificationService: định nghĩa interface Sender, cho SMS/Email implement, tiêm qua constructor.

    ✅ Hoàn thành khi: NotificationService không còn new lớp cụ thể; đổi sang Email không cần sửa nó.

  3. 3

    Tiêm bản giả khi test

    Viết test cho NotificationService dùng một FakeSender ghi lại message thay vì gửi thật.

    ✅ Hoàn thành khi: Test không gọi dịch vụ thật; kiểm được rằng send() đã được gọi với đúng nội dung.

  4. 4

    DIP vs DI

    Viết 3-4 câu phân biệt Dependency Inversion (nguyên tắc) và Dependency Injection (kỹ thuật), kèm ví dụ.

    ✅ Hoàn thành khi: Phân biệt rõ; nêu được DI là một cách thực thi DIP.