[SOLID] [P1] Single Responsibility Principle: Nguyên tắc nền tảng cho clean code
Trong lĩnh vực phát triển phần mềm, việc viết code clean, dễ bảo trì và mở rộng là vô cùng quan trọng. Một trong những nguyên tắc cơ bản giúp đạt được mục tiêu này là Single Responsibility Principle - SRP (Trách nhiệm duy nhất).
Nguyên tắc này khuyến khích việc phân tách các nhiệm vụ, quản lý các dependency và giảm thiểu ảnh hưởng của các thay đổi, thúc đẩy code tập trung, mạch lạc, dễ hiểu, bảo trì và kiểm thử.
Nguyên tắc Single Responsibility, được Robert C. Martin giới thiệu, phát biểu rằng:
“Một class chỉ nên có một lý do duy nhất để thay đổi” (A class should have only one reason to change).
Nói cách khác, một class chỉ nên có một trách nhiệm duy nhất.
Robert C. Martin, người đưa ra thuật ngữ này, định nghĩa SRP là
“Một module chỉ nên chịu trách nhiệm với một, và chỉ một, actor”.
Actor ở đây được hiểu là một nhóm (bao gồm một hoặc nhiều stakeholder hoặc người dùng) yêu cầu thay đổi trong module. Hành vi của phần mềm được xác định bởi người dùng hoặc actor, và actor có thể chịu trách nhiệm thay đổi cách phần mềm hoạt động.
Ví dụ bên dưới mô phỏng một class không tuân theo SRP
class Employee:
"""
Employee class for managing employee data and operations.
"""
def calculate_pay(self):
"""Calculate employee pay"""
pass
def report_hours(self):
"""Report employee worked hours"""
pass
def save(self):
"""Save employee data"""
pass
Với ví dụ trên, nếu phòng kế toán thay đổi chính sách về cách trả lương cho nhân viên, thay đổi này sẽ được phản ánh trong hàm calculate_pay()
. <br>
Cùng ngày hôm đó, nhóm DBA thay đổi cấu trúc bảng user trong database của họ, điều này cũng sẽ được phản ánh trong hàm save()
của cùng một class. Trong cả hai trường hợp, class này có hai lý do để thay đổi, vi phạm SRP ()
Để hiểu rõ hơn, hãy tưởng tượng tại một nhà hàng. Nếu nhà hàng được quản lý hiệu quả:
Mỗi vai trò có một trách nhiệm riêng biệt. Nhà hàng hoạt động mạch lạc, hiệu quả và ít sảy ra vấn đề.
Đầu bếp A vừa nấu ăn, vừa phục vụ bàn, vừa tính tiền. Hậu quả:
Nghe như dự án IT (LOL 😵💫)
Đây là vi phạm nguyên tắc “một người một việc”, dẫn đến hiệu quả kém và rủi ro cao.
Hãy xem xét một ví dụ về class Order vi phạm nguyên tắc SRP:
class Order:
def __init__(self):
self.items = []
self.quantities = []
self.prices = []
self.status = "open"
def add_item(self, name: str, quantity: int, price: float) -> None:
self.items.append(name)
self.quantities.append(quantity)
self.prices.append(price)
def total_price(self):
total = 0
for quantity, price in zip(self.quantities, self.prices):
total += quantity * price
return total
def pay(self, payment_type: str, security_code):
if payment_type == "debit":
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
elif payment_type == "credit":
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
self.status = "paid"
else:
raise Exception(f"Unknown payment type: {payment_type}")
Class này vi phạm SRP vì nó đang làm hai việc: quản lý đơn hàng và xử lý thanh toán.
Đây là cách chúng ta có thể cải thiện code trên bằng cách tách các trách nhiệm:
from dataclasses import dataclass
from typing import List
@dataclass
class OrderItem:
name: str
quantity: int
price: float
def calculate_price(self) -> float:
return self.quantity * self.price
class Order:
def __init__(self):
self.items: List[OrderItem] = []
self.status = "open"
def add_item(self, name: str, quantity: int, price: float) -> None:
self.items.append(OrderItem(name, quantity, price))
def total_price(self) -> float:
return sum(item.calculate_price() for item in self.items)
class PaymentProcessor:
@staticmethod
def process_payment(order: Order, payment_type: str, security_code: str) -> None:
payment_processors = {
"debit": DebitPaymentProcessor(),
"credit": CreditPaymentProcessor()
}
processor = payment_processors.get(payment_type)
if not processor:
raise ValueError(f"Unknown payment type: {payment_type}")
processor.process(security_code)
order.status = "paid"
class DebitPaymentProcessor:
def process(self, security_code: str) -> None:
print("Processing debit payment type")
print(f"Verifying security code: {security_code}")
class CreditPaymentProcessor:
def process(self, security_code: str) -> None:
print("Processing credit payment type")
print(f"Verifying security code: {security_code}")
Bằng cách tách các chức năng thành các class khác nhau, chúng ta có thể dễ dàng thêm các phương thức thanh toán mới mà không cần phải sửa đổi class Order
Vi phạm SRP có thể dẫn đến một số hậu quả tiêu cực, bao gồm:
Một số dấu hiệu cho thấy code của bạn có thể đang vi phạm SRP:
Nguyên tắc Single Responsibility là một nguyên tắc căn bản và mạnh mẽ trong việc thiết kế phần mềm. Bằng cách đảm bảo mỗi class chỉ có một trách nhiệm duy nhất, chúng ta có thể tạo ra các đo code dễ bảo trì, dễ kiểm thử và dễ mở rộng. Điều này không chỉ giúp giảm chi phí phát triển về lâu dài mà còn tạo ra một codebase mạch lạc và chuyên nghiệp.
Hãy nhớ rằng, việc áp dụng SRP không phải là về việc tạo ra càng nhiều class càng tốt, mà là về việc tổ chức code một cách hợp lý, để mỗi thành phần có một mục đích rõ ràng và duy nhất.
Cấp độ | Câu hỏi | Câu trả lời |
---|---|---|
1 | Giải thích nguyên tắc SRP là gì? | SRP là nguyên tắc thiết kế hướng đối tượng, trong đó mỗi class chỉ nên có một lý do duy nhất để thay đổi. Nói cách khác, mỗi class chỉ nên chịu trách nhiệm cho một nhiệm vụ hoặc chức năng cụ thể trong hệ thống. |
1 | Lợi ích của việc áp dụng SRP là gì? | SRP giúp code dễ đọc, dễ bảo trì, dễ kiểm thử và giảm thiểu rủi ro khi thay đổi code. |
1 | Cho ví dụ về SRP trong lập trình. | SRP giúp code dễ đọc, dễ bảo trì, dễ kiểm thử và giảm thiểu rủi ro khi thay đổi code. |
1 | Làm thế nào để nhận biết một class đang vi phạm SRP? | Nếu một class có quá nhiều thuộc tính hoặc phương thức, hoặc nếu tên class chứa các từ nối như “and”, “or”, thì có khả năng class đó đang vi phạm SRP. |
2 | Làm thế nào để refactor một class lớn để tuân thủ SRP? | 1. Xác định các nhiệm vụ: Phân tích class và xác định các nhóm chức năng khác nhau. 2. Tạo các class mới: Tạo các class mới cho mỗi nhiệm vụ đã xác định. 3. Dịch chuyển code: Di chuyển các phương thức và thuộc tính tương ứng sang các class mới. 4. Kiểm tra: Đảm bảo code hoạt động chính xác sau khi refactor. |
2 | Bạn đã từng gặp trường hợp nào áp dụng SRP giúp cải thiện chất lượng code chưa? Hãy chia sẻ. | Trong một dự án trước đây, tôi đã gặp một class ReportGenerator chịu trách nhiệm tạo báo cáo và gửi email báo cáo. Sau khi áp dụng SRP, tôi đã tách riêng chức năng gửi email sang một class EmailSender . Điều này giúp code dễ đọc hơn và dễ dàng thêm các phương thức gửi email khác nhau (ví dụ: gửi qua SMTP, API) mà không ảnh hưởng đến class ReportGenerator . |
2 | Bạn sử dụng chiến lược nào để xác định và định nghĩa trách nhiệm trong một class hoặc module? | Tôi thường sử dụng các kỹ thuật sau: 1. Phân tích tên class/module: Tên class/module nên phản ánh rõ ràng trách nhiệm của nó. 2. Phân tích các phương thức: Các phương thức trong class/module nên liên quan đến một chức năng cụ thể. 3. Tìm kiếm các dấu hiệu vi phạm SRP: Ví dụ: class có quá nhiều thuộc tính/phương thức, tên class chứa các từ nối. |