Để khắc phục điều đó, bộ nguyên lý SOLID được đề xuất bởi Robert C. Martin (Uncle Bob) nhằm giúp lập trình viên xây dựng hệ thống dễ mở rộng, dễ sửa chữa và dễ kiểm thử.
Áp dụng SOLID sẽ giúp:
1. Single Responsibility Principle – Một lý do duy nhất để thay đổi
Mỗi class nên chỉ có một nhiệm vụ chính. Nếu một class đảm nhận nhiều việc, việc thay đổi một chức năng có thể làm ảnh hưởng tới các chức năng khác.
Ví dụ tốt:
// Tách rõ trách nhiệm
class InvoicePrinter {
print(invoice: Invoice) { /* logic in ra hoá đơn */ }
}
class InvoiceSaver {
save(invoice: Invoice) { /* logic lưu hoá đơn vào DB */ }
}
Ví dụ chưa tốt:
// Gộp quá nhiều chức năng
class InvoiceHandler {
print(invoice: Invoice) { ... }
save(invoice: Invoice) { ... }
sendEmail(invoice: Invoice) { ... }
}
Ở ví dụ sau, class InvoiceHandler
gộp nhiều trách nhiệm, khiến nó trở nên khó bảo trì.
2. Open/Closed Principle – Mở để mở rộng, đóng để chỉnh sửa
Thay vì thay đổi code cũ khi có tính năng mới, hãy thiết kế sao cho có thể mở rộng mà không ảnh hưởng đến phần hiện có.
Ví dụ:
interface PaymentMethod {
pay(amount: number): void;
}
class CreditCard implements PaymentMethod {
pay(amount: number) { console.log("Paid by credit card"); }
}
class Paypal implements PaymentMethod {
pay(amount: number) { console.log("Paid by Paypal"); }
}
function checkout(method: PaymentMethod) {
method.pay(100);
}
Khi muốn thêm phương thức thanh toán khác, chỉ cần viết thêm class mới mà không cần thay đổi hàm checkout
.
3. Liskov Substitution Principle – Thay thế mà không phá vỡ
Class con cần tuân thủ hành vi của class cha để có thể được sử dụng thay thế mà không làm sai lệch kết quả mong đợi.
Ví dụ chưa đúng:
4. Interface Segregation Principle – Chỉ sử dụng đúng thứ cần
Đừng ép các class phải triển khai những phương thức không liên quan đến nhiệm vụ của chúng. Interface nên được chia nhỏ theo chức năng cụ thể.
Ví dụ chưa tốt:
interface Machine {
print(): void;
scan(): void;
fax(): void;
}
Một chiếc máy in đơn giản không cần scan hay fax, nhưng vẫn buộc phải implement nếu dùng interface này.
Giải pháp:
interface Printer {
print(): void;
}
interface Scanner {
scan(): void;
}
5. Dependency Inversion Principle – Ưu tiên phụ thuộc vào trừu tượng
Các module cấp cao không nên gắn chặt vào chi tiết cụ thể của module cấp thấp. Thay vào đó, cả hai nên phụ thuộc vào các abstraction (giao diện, lớp trừu tượng).
Ví dụ đúng cách:
Bằng cách này, bạn có thể dễ dàng thay thế EmailService
bằng dịch vụ khác như SMS hoặc push notification mà không cần chỉnh sửa Notification
.
Kết luận
SOLID không phải là bộ quy tắc bắt buộc, nhưng là kim chỉ nam giúp bạn viết code rõ ràng, dễ phát triển và ít lỗi hơn. Khi áp dụng thường xuyên, bạn sẽ cảm nhận rõ hệ thống trở nên linh hoạt, dễ test và bền vững hơn trong dài hạn.