bài viết ngẫu nhiên
Bạn có bao giờ sửa code ở một chỗ → xong kéo theo lỗi dây chuyền ở chỗ khác không?
Ví dụ: thay đổi cách lưu dữ liệu thì toàn bộ logic User, Product, Order đều phải sửa lại. Cảm giác như chạm một cọng rễ cây mà cả cái cây rung lắc vậy 🌳.
Nguyên nhân thường là do code của bạn phụ thuộc trực tiếp vào chi tiết (implementation) thay vì phụ thuộc vào abstraction (interface).
Đây chính là chỗ Dependency Inversion Principle (DIP) xuất hiện để giải cứu.
Định nghĩa ngắn gọn:
Module cấp cao không nên phụ thuộc vào module cấp thấp. Cả hai nên phụ thuộc vào abstraction.
Module cấp cao: chứa logic nghiệp vụ chính (business logic).
Module cấp thấp: chứa chi tiết kỹ thuật (database, API, file system...).
Abstraction: lớp trung gian (interface, abstract class) để cả hai cùng “nói chuyện” với nhau.
Nói đơn giản: thay vì để UserService “ôm chặt” MySQL, hãy để nó “ôm” một interface Database. MySQL, PostgreSQL, hay MongoDB chỉ việc đứng sau interface đó mà phục vụ thôi.
class MySQLDatabase {
save(data: string) {
console.log("Saving to MySQL:", data);
}
}
class UserService {
private db = new MySQLDatabase();
addUser(user: string) {
this.db.save(user);
}
}Ở đây:
UserService phụ thuộc trực tiếp vào MySQLDatabase.
Nếu bạn muốn đổi sang PostgreSQL hoặc lưu vào Redis thì sao? → Sửa code trong UserService ngay lập tức.
Việc test cũng khó khăn vì UserService lúc nào cũng dính với MySQLDatabase thật.
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string) {
console.log("Saving to MySQL:", data);
}
}
class PostgreSQLDatabase implements Database {
save(data: string) {
console.log("Saving to PostgreSQL:", data);
}
}
class UserService {
constructor(private db: Database) {}
addUser(user: string) {
this.db.save(user);
}
}
UserService giờ không quan tâm đến MySQL hay PostgreSQL.
Khi test, bạn có thể truyền vào một MockDatabase để giả lập hành vi lưu dữ liệu.
Thay đổi backend lưu trữ chỉ cần viết thêm class mới implement Database, không phải sửa code cũ.
NestJS vốn đã áp dụng DIP một cách tự nhiên nhờ Dependency Injection (DI).
@Injectable()
class UserService {
constructor(private readonly db: Database) {}
addUser(user: string) {
this.db.save(user);
}
}Trong AppModule, bạn chỉ cần bind interface với implementation:
@Module({
providers: [
UserService,
{ provide: 'Database', useClass: MySQLDatabase },
],
})
export class AppModule {}
Muốn đổi sang PostgreSQL? Chỉ việc thay useClass. Code business không hề động tới.
Dễ bảo trì: thay đổi chi tiết kỹ thuật mà không ảnh hưởng đến logic chính.
Dễ test: inject mock hoặc fake thay vì dùng implementation thật.
Dễ mở rộng: thêm database, API provider, storage mới mà không sửa code cũ.
Code “clean” hơn: phân tách rõ ràng giữa business logic và technical detail.
DIP rất hữu ích, nhưng nếu project của bạn:
Rất nhỏ (ví dụ todo app cá nhân).
Chỉ dùng duy nhất một loại database, không có nhu cầu thay đổi.
Thì việc tạo thêm abstraction có thể làm code “phức tạp hóa vấn đề”.
Hãy nhớ: nguyên tắc chỉ là công cụ, không phải luật bất di bất dịch.
Dependency Inversion Principle giúp code của bạn bớt “dính chặt”, linh hoạt và dễ bảo trì hơn.
Hãy tưởng tượng abstraction là “ổ cắm điện”, còn implementation là các loại “phích cắm”.
Miễn bạn cắm vào đúng ổ, thì đó là bàn là, tủ lạnh, hay sạc điện thoại đều chạy được 🔌.
Lần sau khi viết một class, hãy tự hỏi:
Nó đang phụ thuộc vào interface hay class cụ thể?
Nếu ngày mai tôi đổi database/API, code của tôi có dễ đổi không?
Nếu câu trả lời là “có thể đổi thoải mái”, thì xin chúc mừng: bạn đã đi đúng hướng DIP rồi! 🎉
Bạn có muốn mình viết thêm demo code Unit Test với mock database để minh họa lợi ích DIP trong testing không?