Nguyên Tắc Thiết Kế Phần Mềm: Ví Dụ Thực Tiễn Với Python
Tóm tắt: Trong bài viết này, tôi sẽ giải thích các nguyên tắc thiết kế phần mềm quan trọng (SOLID - nhấn mạnh vào SRP và DIP, DRY, KISS, YAGNI) và trình bày một ví dụ thực tiễn đơn giản trong Python: dịch vụ thông báo (email + SMS) được thiết kế để có thể mở rộng, dễ kiểm tra và dễ hiểu.
Các nguyên tắc đã chọn
- SOLID (SRP, OCP, LSP, ISP, DIP) - nhấn mạnh vào SRP và DIP.
- DRY (Don't Repeat Yourself).
- KISS (Keep It Simple, Stupid).
- YAGNI (You Aren't Gonna Need It).
- Phân tách trách nhiệm / Khả năng kiểm tra / Tính mô-đun.
Vấn đề thực tế
Chúng ta cần một thành phần gửi thông báo đến người dùng qua nhiều kênh (ví dụ: email và SMS). Các yêu cầu thực tiễn:
- Có khả năng thêm các kênh mới (Push, Webhook) mà không thay đổi logic chính.
- Giúp việc kiểm tra đơn vị dễ dàng mà không cần gọi mạng thực.
- Giữ cho mã nguồn rõ ràng và phân tách trách nhiệm.
Thiết kế (tóm tắt)
- Định nghĩa một trừu tượng
Notifierđại diện cho hợp đồng gửi thông báo. - Các triển khai cụ thể (
EmailNotifier,SMSNotifier) thực hiện trừu tượng. NotificationServiceđiều phối các notifier thông qua việc tiêm phụ thuộc (nó không biết về các triển khai cụ thể).- Các khách hàng bên ngoài (SMTP, nhà cung cấp SMS) được đóng gói và giả lập trong các bài kiểm tra.
Ví dụ mã (các tệp gợi ý: notifiers.py, test_notifiers.py)
```python
# notifiers.py
from abc import ABC, abstractmethod
from typing import List, Dict
class Notifier(ABC):
"""Hợp đồng: bất kỳ Notifier nào phải triển khai phương thức send."""
@abstractmethod
def send(self, to: str, subject: str, body: str) -> bool:
pass
class EmailNotifier(Notifier):
"""SRP: lớp này chỉ biết cách gửi email."""
def __init__(self, smtp_client):
# smtp_client đóng gói logic gửi thực tế
self.smtp = smtp_client
def send(self, to: str, subject: str, body: str) -> bool:
return self.smtp.send_email(to, subject, body)
class SMSNotifier(Notifier):
def __init__(self, sms_client):
self.sms = sms_client
def send(self, to: str, subject: str, body: str) -> bool:
# Đơn giản hóa: sử dụng subject làm tiền tố trong SMS
text = f"{subject}: {body}"
return self.sms.send_sms(to, text)
class NotificationService:
"""DIP: phụ thuộc vào trừu tượng Notifier, không phải các lớp cụ thể."""
def __init__(self, notifiers: List[Notifier]):
self.notifiers = notifiers
def notify_all(self, to: str, subject: str, body: str) -> Dict[str, bool]:
results = {}
for n in self.notifiers:
key = n.__class__.__name__
results[key] = n.send(to, subject, body)
return results
# Các client giả lập cho demo/local
class MockSMTPClient:
def send_email(self, to, subject, body):
print(f"[MockSMTP] Gửi email đến {to}: {subject} / {body}")
return True
class MockSMSClient:
def send_sms(self, to, text):
print(f"[MockSMS] Gửi SMS đến {to}: {text}")
return True
if __name__ == "__main__":
email_notifier = EmailNotifier(MockSMTPClient())
sms_notifier = SMSNotifier(MockSMSClient())
svc = NotificationService([email_notifier, sms_notifier])
result = svc.notify_all("user@example.com", "Chào mừng", "Xin chào, cảm ơn bạn đã đăng ký.")
print("Kết quả:", result)
```
Kiểm tra (ví dụ với pytest - test_notifiers.py)
```python
# test_notifiers.py
from notifiers import Notifier, NotificationService
class DummyNotifier(Notifier):
def __init__(self):
self.sent = False
def send(self, to, subject, body):
self.sent = True
return True
def test_notification_service_sends_to_all():
a = DummyNotifier()
b = DummyNotifier()
svc = NotificationService([a, b])
res = svc.notify_all("u@x.com", "t", "b")
assert res["DummyNotifier"] is True
assert a.sent and b.sent
```
Cách các nguyên tắc áp dụng ở đây
- SRP (Single Responsibility): Mỗi lớp có một trách nhiệm duy nhất:
EmailNotifierchỉ gửi email,SMSNotifierchỉ gửi SMS,NotificationServicechỉ điều phối. - DIP (Dependency Inversion):
NotificationServicephụ thuộc vàoNotifier(trừu tượng), không phải các triển khai cụ thể. Điều này cho phép tiêm các đối tượng giả lập để kiểm tra. - OCP (Open/Closed): Để thêm
PushNotifier, bạn không cần sửa đổiNotificationService; chỉ cần tạo một triển khai mới củaNotifiervà đăng ký nó. - LSP & ISP: Các triển khai tôn trọng hợp đồng
sendvà không buộc phải có các phương thức không cần thiết. - DRY: Logic cụ thể của phương tiện được đóng gói, tránh sự trùng lặp.
- KISS / YAGNI: Thiết kế đơn giản đáp ứng các yêu cầu hiện tại; không có việc thử lại, nhóm lại, hoặc thêm độ phức tạp cho đến khi cần thiết.
- Khả năng kiểm tra / Tính mô-đun: Bằng cách tiêm các phụ thuộc và sử dụng các client giả lập, các bài kiểm tra có tính xác định và nhanh chóng.
Cách chạy cục bộ
-
Tạo một môi trường ảo (khuyến nghị):
python -m venv .venv source .venv/bin/activate # Linux/macOS .venv\Scripts\activate # Windows pip install pytest -
Chạy demo:
python notifiers.py -
Chạy các bài kiểm tra:
pytest -q
Kết luận
Ví dụ đơn giản này chứng minh cách áp dụng các nguyên tắc thiết kế để xây dựng một thành phần thông báo có khả năng mở rộng, dễ kiểm tra và dễ bảo trì. Bằng cách ưu tiên các trừu tượng, phân tách trách nhiệm và sự đơn giản, mã nguồn đã sẵn sàng để phát triển (thêm các kênh, công cụ đo lường, thử lại) mà không trở nên mong manh.