IoC, DI và DIP trong lập trình backend
Tóm tắt một vài nội dung xoay quanh IoC, DI và DIP trong lập trình backend, giúp bạn hiểu hơn về những thuật ngữ quan trọng
1. Inversion of Control - Sự trừu tượng hoá luồng điều khiển
Về mặc lý thuyết, IoC là một nguyên lý thiết kế mà trong đó luồng điều khiển (flow of control) của một chương trình bị đảo ngược so với lập trình cấu trúc truyền thống.
Trong lập trình truyền thống: Mã nguồn của nhà phát triển (high-level code) trực tiếp gọi các thư viện hoặc các hàm để thực thị logic. Nhà phát triển nắm quyền kiểm soát toàn bộ vòng đời của thực thể.
Trong IoC: Một framework hoặc một thành phần hạ tầng đóng vai trò là chuơng trình chính. Nó sẽ triệu hồi (call back) mã nguồn của nhà phát triển tại các thời điểm mở rộng (extension points) đã định nghĩa trước.
Định nghĩa hàn lâm: IoC là sự dịch chuyển trách nhiệm quản lý luồng thực thi và vòng đời đối tượng từ mã nguồn ứng dụng sang một container hoặc framework.
2. Dependency Injection (DI) - Hiện thực hoá sự lỏng lẻo (Loose Coupling)
DI được coi là một mẫu hình đặc biệt của IoC, tập trung vào việc quản lý các mối quan hệ (dependencies) giữa các thành phần.
Theo quan điểm hệ thống, một đối tượng không nên có tri thức về cách cấu hình hoặc khởi tạo sự phụ thuộc của nó. Thay vào đó, trách nhiệm này được tách rời (decoupled) và chuyển giao cho một thực thể bên ngoài (injector).
Có 3 thành phần trong mô hình DI:
Client: Đối tượng cần sử dụng dịch vụ
Service (Dependency): Đối tượng cung cấp dịch vụ
Injector: Thành phần thực hiện việc cài đặt (inject) Service vào Client
3. Mối quan hệ với Nguyên lý Nghịch đảo Phụ thuộc (Dependency Inversion Principle - DIP)
Để hiểu sâu về IoC/DI ở mức hàn lâm, ta phải nhắc đến DIP (D trong nguyên lý SOLID). DIP đưa ra hai quy tắc cốt lõi:
Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hau đề phải phụ thuộc và sự trừu tượng (Abstractions)
Sự trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết phải phụ thuộc và sự trừu tượng
Lập luận: DI chính là kỹ thuật để thực hiện DIP. Bằng cách “tiêm“ một interface vào constructor, ta đảm bảo module cấp cao chỉ giao tiếp với sự trừu tượng, từ đó triệt tiêu sự phụ thuộc và cài đặt cụ thể (Concrete Implementation).
HighLevelModule → Interface ← LowLevelModule
4. Tổng kết
IoC là một Meta-pattern (mẫu hình cao cấp) định nghĩa lại quyền hạn của các thành phần trong hệ thống.
DI là một design pattern cụ thể hoá việc chuyển giao trách nhiệm khởi tạo đối tượng
IoC Container là một Management Layer (lớp quản lý) chịu trách nhiệm thực thi các quy tắc về phạm vi (Scope - Singleton, Transient, Scoped) và vòng đời (Lifecycle) của các thành phần trong hệ thống.
5. Minh hoạ
interface MessageService {
send(message: string): string;
}
class SmsService implements MessageService {
send(message: string) {
return 'Sms Sent:' + message;
}
}
class EmailService implements MessageService {
send(message: string) {
return 'Email Sent: ' + message;
}
}
class NotificationService {
private messageService: MessageService;
constructor(messageService: MessageService) {
this.messageService = messageService;
}
sendAuth(message: string) {
console.log('Sending...');
const result = this.messageService.send(message);
console.log(result);
}
}
const emailService = new EmailService();
const authService = new NotificationService(emailService);
authService.sendAuth('Hello, you got fired');Nhìn vào đoạn code mẫu này, chúng ta sẽ thấy nó có giá trị khi chúng ta cần bổ sung hoặc thay đổi những hình thức gửi thông báo. Ví dụ chúng ta cần thêm hình thức gửi qua Telegram, chỉ cần thêm class TelegramService, sau đó sử dụng một cách điệu nghệ phương thức mới mà không cần phải sửa bất kỳ dòng code nào của NotificationService :)


