[SOLID] [P1] Single Responsibility Principle: Nguyên tắc nền tảng cho clean code

Fri, December 20, 2024 - 9 min read View Count
Single Responsibility Principle Diagram

Xem những bài viết cùng series

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ử.  

SRP là gì và tại sao nó quan trọng?

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ả:

  • Đầu bếp tập trung vào nấu ăn
  • Bồi bàn chuyên về phục vụ khách hàng
  • Quản lý lo về công việc hành chính

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 đề. 1734972954146

Ví dụ về một nhà hàng không tuân thủ SRP

Đầ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ả:

  • Món ăn bị cháy vì đang nấu phải chạy ra phục vụ khách
  • Khách đợi lâu vì đầu bếp đang bận nấu món khác
  • Tính tiền sai vì vừa phải để ý bếp vừa tính toán
  • Stress và kiệt sức vì làm quá nhiều việc
  • Không thể tập trung chuyên môn để nâng cao tay nghề nấu ăn

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.

Ví dụ thực tế về vi phạm SRP

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.

Cách áp dụng SRP đúng

Đâ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

Lợi ích của việc áp dụng SRP

  1. Tăng tính module hóa: Code được tổ chức thành các module nhỏ, tập trung vào một nhiệm vụ cụ thể.
  2. Dễ dàng bảo trì: Khi mỗi class chỉ có một nhiệm vụ, việc sửa đổi và nâng cấp trở nên đơn giản hơn.
  3. Tăng khả năng kiểm thử: Các module nhỏ, tập trung dễ dàng kiểm thử hơn.
  4. Code rõ ràng hơn: Mỗi class có một mục đích rõ ràng, giúp người khác dễ dàng hiểu và làm việc với code.
  5. Giảm rủi ro: Thay đổi trong một class ít có khả năng ảnh hưởng đến các phần khác của hệ thống.

Hậu quả của việc vi phạm SRP

1734972794023 Vi phạm SRP có thể dẫn đến một số hậu quả tiêu cực, bao gồm:

  • Phần mềm khó triển khai hơn và có thể dẫn đến các tác dụng phụ không mong muốn: Khi một class có nhiều nhiệm vụ, việc thay đổi code liên quan đến một nhiệm vụ có thể ảnh hưởng đến các trách nhiệm khác, gây ra lỗi khó lường.
  • Khó giải thích, hiểu, triển khai và kiểm tra: Các class có quá nhiều nhiệm vụ thường phức tạp và khó quản lý, làm tăng thời gian và công sức cần thiết để phát triển và bảo trì phần mềm.
  • “God Classes”: Các class có quá nhiều chức năng, nhiệm vụ được gọi là “God Classes” vì chúng làm quá nhiều việc và biết quá nhiều thứ. Những class này thường rất lớn, chứa hàng nghìn dòng code, và rất khó bảo trì vì nguy cơ gây ra lỗi khi thay đổi code là rất cao 1734946122552

Cách áp dụng SRP

  1. Xác định trách nhiệm, nhiệm vụ: Phân tích class của bạn để xác định các trách nhiệm, nhiệm vụ khác nhau. Mỗi nhiệm vụ là một trục thay đổi tiềm năng.
  2. Chia để trị: Nếu một class có nhiều hơn một trách nhiệm, hãy xem xét chia nó thành các class nhỏ hơn, mỗi class có trách nhiệm riêng.
  3. Tái sử dụng code: Tận dụng các class hiện có để thực hiện các chức năng chung, tránh trùng lặp code.
  4. Đặt tên rõ ràng: Đặt tên cho các class và phương thức sao cho phản ánh rõ ràng trách nhiệm của chúng.
  5. Kiểm tra lại code: Thường xuyên kiểm tra lại code của bạn để đảm bảo rằng mỗi class chỉ có một trách nhiệm duy nhất.

Cách nhận biết sự vi phạm SRP

Một số dấu hiệu cho thấy code của bạn có thể đang vi phạm SRP:

  1. Class có quá nhiều dòng code
  2. Class phụ thuộc vào nhiều class khác
  3. Các method trong class xử lý các công việc không liên quan
  4. Thay đổi một chức năng yêu cầu sửa đổi nhiều nơi trong code

Kết luận

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.

Một số câu hỏi phòng vấn về SRP (Tham khảo 🤪)

Cấp độCâu hỏiCâu trả lời
1Giả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.
1Lợ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.
1Cho 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.
1Là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.
2Là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.
2Bạ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.
2Bạ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.

Random Meme (😂)

Random Meme