Đây là bài viết thuộc chuỗi NestJS thực chiến, bạn có thể tìm đọc toàn bộ chuỗi bài viết tại đây: NestJS thực chiến.
Giới thiệu 📜
S.O.L.I.D là một cụm từ mà mọi lập trình viên đều đã từng nghe qua. Các nguyên tắc này giúp chúng ta viết mã sạch, dễ duy trì, dễ mở rộng và rõ ràng hơn. Tuy nhiên, không phải ai cũng có thể diễn đạt một cách đầy đủ về S.O.L.I.D. Để giúp bạn hiểu rõ hơn, bài viết này sẽ đi sâu vào từng nguyên tắc với ví dụ cụ thể trong các dự án thực tế.
Thông tin package 📦️
- Mã nguồn của phần S và I sẽ nằm ở branch
part-11-upload-file-aws-s3-client
. - Mã nguồn ví dụ cho L có thể tìm thấy dưới đây.
Nội dung 💡
S: Nguyên Tắc Trách Nhiệm Đơn (Single Responsibility Principle - SRP)
Mỗi lớp chỉ nên có một lý do duy nhất để thay đổi.
Vấn đề ⚠️
Ví dụ dưới đây sẽ giúp bạn hiểu được nguyên tắc SRP:
typescript
class Topic {
createTopic(data: CreateTopicDto) {...}
updateTopic(id: string, data: UpdateTopicDto) {...}
deleteTopic(id: string) {...}
getAllTopics(filter: GetAllFilter) {...}
sendNotificationToSubscribers(id: string, content: string) {...} // Vi phạm SRP
addNewSubscriber(id: string, user_id: string) {...} // Vi phạm SRP
}
Như bạn thấy, hai phương thức sendNotificationToSubscribers
và addNewSubscriber
vi phạm nguyên tắc SRP vì chúng không thuộc trách nhiệm chính của lớp Topic. Việc này có thể dẫn tới tình trạng khó khăn khi cần thay đổi logic, vì mọi thay đổi đều phải thực hiện trong lớp Topic.
Giải pháp 💡
Để giải quyết, chúng ta nên phân chia lớp thành các chức năng riêng:
typescript
class Topic {
createTopic(data: CreateTopicDto) {...}
updateTopic(id: string, data: UpdateTopicDto) {...}
deleteTopic(id: string) {...}
getAllTopics(filter: GetAllFilter) {...}
}
class Subscriber {
addNewSubscriber(id: string, user_id: string) {...}
}
class Notifier {
sendNotificationToSubscribers(id: string, content: string) {...}
}
Phân chia như vậy giúp mã nguồn trở nên module, dễ đọc và dễ bảo trì hơn.
O: Nguyên Tắc Đóng/Mở (Open/Closed Principle - OCP)
Thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi.
Vấn đề ⚠️
Khi bạn có nhiều điều kiện trong code như if-else
hay switch-case
, việc này có thể dẫn đến khó khăn khi thêm chức năng mới. Xem ví dụ dưới đây:
typescript
class PaymentService {
public processPayment(method: PaymentMethod, orderData: any) {
switch (method){
case "Card":
break;
case "ApplePay":
break;
// ...
}
}
}
Việc thêm phương thức thanh toán mới sẽ mất rất nhiều thời gian và dễ gây ra lỗi nếu không chú ý.
Giải pháp 💡
Thay vì sửa đổi mã cũ, hãy mở rộng với các lớp mới:
typescript
interface Payment {
processPayment(method: PaymentMethod, orderData: any);
}
class CardPayment implements Payment {...}
class ApplePayPayment implements Payment {...}
// ...
class PaymentService {
private payments: Record<string, Payment> = {};
}
Cách làm này giúp bạn dễ dàng thêm phương thức thanh toán mới mà không làm ảnh hưởng tới mã nguồn cũ.
L: Nguyên Tắc Thay Thế Liskov (Liskov Substitution Principle - LSP)
Mọi thể hiện của lớp con phải có khả năng thay thế cho lớp cha mà không làm sai lệch chương trình.
Giải pháp 💡
Ví dụ dưới đây cho thấy sự cần thiết của nguyên tắc này:
typescript
abstract class Vehicle {
abstract accelerate(): void;
}
class Sedan extends Vehicle {...}
class Bicycle extends Vehicle {...}
Với định nghĩa như vậy, Bicycle không thể thực hiện một số hành vi của Vehicle, điều này vi phạm LSP. Hãy tách rời các hành vi này để đảm bảo tính chính xác và nhất quán của chương trình.
I: Nguyên Tắc Phân Tách Giao Diện (Interface Segregation Principle - ISP)
Một lớp không nên bị ép buộc phải thực hiện các giao diện và phương thức mà nó không sử dụng.
Giải pháp 💡
typescript
interface Vehicle {
accelerate(): void;
}
interface Engine {
turnOnEngine(): void;
}
class Bicycle implements Vehicle {...}
Tách rời các giao diện giúp tránh các lỗi không mong muốn và làm cho mã nguồn trở nên gọn gàng, dễ đọc hơn.
D: Nguyên Tắc Đảo Ngược Phụ Thuộc (Dependency Inversion Principle - DIP)
Các module cấp cao không nên phụ thuộc vào các module cấp thấp, cả hai đều nên phụ thuộc vào trừu tượng.
Giải pháp 💡
typescript
interface TopicRepositoryInterface {...}
class TopicService {
constructor(repository: TopicRepositoryInterface) {...}
}
Việc sử dụng DIP giúp mã nguồn linh hoạt hơn, dễ kiểm tra và bảo trì.
Kết luận 📝
Qua bài viết này, chúng ta đã cùng nhau tìm hiểu về nguyên tắc S.O.L.I.D và ứng dụng của chúng trong lập trình. Hy vọng những kiến thức này sẽ hữu ích cho bạn trong quá trình phát triển ứng dụng.
Xin cảm ơn bạn đã dành thời gian đọc bài viết. Hẹn gặp lại trong các bài viết tiếp theo 🎉
Tài liệu tham khảo 🔍
- Uehara, K.T. (2024) Solid - the simple way to understand, DEV Community. Link bài viết.
- Thelma, U. (2020) The S.O.L.I.D principles in pictures, Medium. Link bài viết.
Thay đổi 📓
- Ngày 24 tháng 5 năm 2024: Tạo tài liệu.
- Ngày 28 tháng 5 năm 2024: Cập nhật tài liệu và xuất bản.
source: viblo