Nếu bạn là một lập trình viên, có lẽ bạn đã từng nghe những lời thì thầm về trí tuệ cổ xưa này trong các buổi review code, tài liệu thiết kế, hoặc trong những cuộc trò chuyện khe khẽ giữa hai lập trình viên kỳ cựu ở một góc văn phòng:
"Bạn nên tuân theo các nguyên tắc SOLID."
Nhưng chính xác thì chúng là gì? Một hội kín bí mật nào đó? Một framework JavaScript mới? Đừng lo — SOLID đơn giản chỉ là một từ viết tắt, và là một trong những bản thiết kế tốt nhất để viết ra những đoạn code dễ bảo trì, dễ mở rộng, và... ít có nguy cơ trở thành thảm họa.
Hãy cùng phân tích từng nguyên tắc, với chút hài hước từ đời thực.
S — Nguyên tắc trách nhiệm đơn nhất (Single Responsibility Principle - SRP)
"Một class chỉ nên có một lý do để thay đổi."
Ví dụ đời thực:
- Hãy tưởng tượng bạn thuê một thợ sửa ống nước đến để sửa cái bồn rửa, nhưng đang làm dở thì anh ta bắt đầu thuyết giảng cho bạn về kế hoạch thuế. Đó chính là cảm giác khi một đoạn code vi phạm SRP.
VD code không tốt:
class UserManager { public void createUser() { /* ... */ } public void deleteUser() { /* ... */ } public void generateUserReport() { /* ... */ } // 🚨 Mixing concerns!
}
Nên sửa lại thế này:
class UserManager { public void createUser() { /* ... */ } public void deleteUser() { /* ... */ }
}
class UserReportGenerator { public void generateUserReport() { /* ... */ }
}
Mỗi class hiện có một chức năng. Ít tác dụng phụ bất ngờ hơn, ít đau đầu hơn.
O — Nguyên tắc Mở/Đóng (Open/Closed Principle - OCP)
"Các thực thể phần mềm nên mở để mở rộng, nhưng đóng để chỉnh sửa."
Ví dụ đời thực:
- Khi điện thoại của bạn có tính năng mới, bạn chỉ cần cài thêm ứng dụng. Bạn không cần mang tua vít ra tháo tung bo mạch để gắn thêm linh kiện. Code của bạn cũng nên hoạt động theo cách như vậy.
VD code không tốt:
class NotificationService { public void send(String message, String type) { if (type.equals("Email")) { /* send email */ } else if (type.equals("SMS")) { /* send SMS */ } }
}
Nên sửa lại thế này:
interface NotificationSender { void send(String message);
} class EmailSender implements NotificationSender { public void send(String message) { /* send email */ }
} class SMSSender implements NotificationSender { public void send(String message) { /* send SMS */ }
} class NotificationService { private NotificationSender sender; public NotificationService(NotificationSender sender) { this.sender = sender; } public void notify(String message) { sender.send(message); }
}
Kiểu mới? Chỉ cần thêm một lớp mới. Logic cốt lõi của bạn vẫn được giữ nguyên, mức độ căng thẳng vẫn ở mức thấp.
L — Nguyên tắc thay thế Liskov (Liskov Substitution Principle - LSP)
"Các đối tượng của lớp con nên có khả năng thay thế cho đối tượng của lớp cha mà không làm sai lệch hành vi chương trình."
Ví dụ đời thực:
- Khi bạn thuê một chiếc xe, bạn mong là có thể lái nó — dù là sedan, SUV hay mui trần. Nếu công ty cho thuê giao cho bạn một chiếc xuồng có gắn bánh xe, chắc bạn sẽ tức điên lên.
VD code không tốt:
class Bird { public void fly() { /* flying logic */ }
} class Penguin extends Bird { public void fly() { throw new UnsupportedOperationException("Penguins can't fly!"); }
}
Nên sửa lại thành:
interface Bird { void eat();
} interface FlyingBird extends Bird { void fly();
} class Sparrow implements FlyingBird { public void eat() { /* eat */ } public void fly() { /* fly */ }
} class Penguin implements Bird { public void eat() { /* eat */ }
}
Giờ Penguin không còn phải giả vờ là thứ mà chúng không muốn nữa. Ít ngoại lệ hơn, ít lừa dối đi.
I — Nguyên tắc tách giao diện (Interface Segregation Principle - ISP)
"Không khách hàng nào nên bị ép phải phụ thuộc vào những phương thức mà họ không sử dụng."
Ví dụ đời thực:
- Bạn gọi một ly cà phê, nhưng người ta bưng ra nguyên một bữa ăn 10 món – dù bạn có muốn hay không.
VD code không tốt:
interface Worker { void work(); void eat();
} class Robot implements Worker { public void work() { /* working... */ } public void eat() { /* ??? Robots don't eat! */ }
}
Nên làm thế này:
interface Workable { void work();
} interface Eatable { void eat();
} class Human implements Workable, Eatable { public void work() { /* work */ } public void eat() { /* eat */ }
} class Robot implements Workable { public void work() { /* work */ }
}
Bây giờ mỗi lớp chỉ triển khai những gì nó thực sự cần. Đơn giản. Sạch sẽ. Hợp lý.
D — Nguyên tắc Đảo ngược Sự phụ thuộc (Dependency Inversion Principle - DIP)
"Hãy phụ thuộc vào các abstract, chứ không phải các cụ thể."
Ví dụ đời thực:
- Nếu điện thoại của bạn chỉ sạc được bằng đúng một loại sạc duy nhất, bạn sẽ nổi đóa. May mà nó dùng cổng USB hay sạc không dây — một dạng trừu tượng.
VD không tốt:
class MySQLDatabase { public void connect() { /* ... */ }
} class UserRepository { private MySQLDatabase db = new MySQLDatabase(); public void saveUser() { db.connect(); /* save logic */ }
}
Nên làm thế này thì tốt hơn:
interface Database { void connect();
} class MySQLDatabase implements Database { public void connect() { /* ... */ }
} class UserRepository { private Database db; public UserRepository(Database db) { this.db = db; } public void saveUser() { db.connect(); /* save logic */ }
}
Bây giờ bạn có thể hoán đổi cơ sở dữ liệu như thay áo. Dependency injection = tự do.
Kết luận
Các nguyên tắc SOLID giống như luật giao thông cho code của bạn. Chúng không ngăn bạn viết ra mớ hỗn độn, nhưng sẽ cung cấp cho bạn bản đồ để thiết kế hệ thống an toàn, dễ mở rộng và dễ bảo trì hơn.
Nếu bạn bắt đầu thấy rằng:
- Các class của bạn làm quá nhiều việc,
- Thêm một tính năng mới làm hỏng bốn cái khác,
- Hoặc code khiến bạn muốn giả chết và bắt đầu lại cuộc đời...
...thì khả năng cao là bạn đang vi phạm một (hoặc vài) nguyên tắc trong SOLID.
Hãy gắn bó với chúng, và tương lai của bạn — cũng như đồng nghiệp của bạn — sẽ âm thầm cảm ơn bạn.