Facebook Pixel

SOLID là gì? Ví dụ dễ hiểu về nguyên lý SOLID cho người mới

22 Aug, 2024

SOLID trong OOP là tập hợp các nguyên lý thiết kế phần mềm giúp lập trình viên tạo ra các hệ thống dễ bảo trì, dễ mở rộng và có tính ổn định

SOLID là gì? Ví dụ dễ hiểu về nguyên lý SOLID cho người mới

Mục Lục

1. SOLID là gì?

SOLID trong lập trình hướng đối tượngtập hợp các nguyên lý thiết kế phần mềm nhằm giúp lập trình viên tạo ra các hệ thống dễ bảo trì, dễ mở rộng và có tính ổn định. Các nguyên lý này bao gồm: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation và Dependency Inversion.

2. Các nguyên lý trong SOLID

2.1 Single Responsibility Principle (SRP)

Single Responsibility Principle (SRP) là nguyên lý đầu tiên của SOLID trong lập trình hướng đối tượng, ý nghĩa của nó có thể được hiểu như sau:

  • Mỗi lớp (class) trong chương trình chỉ nên có một trách nhiệm duy nhất.
  • Hay nói cách khác, một lớp chỉ nên có một lý do duy nhất để thay đổi.

Khi một class có nhiều trách nhiệm, nó sẽ trở nên khó quản lý hơn, khó kiểm tra lỗi, mở rộng và bảo trì. Bạn có thể nhìn vào class OrderManagement trước khi áp dung SRP, class này có quá nhiều nhiệm vụ, từ xử lý đơn hàng, tính toán, đến in hoá đơn. Nếu cần thay đổi logic tính toán hoặc định dạng hóa đơn, ta phải thay đổi toàn bộ lớp này, điều này sẽ làm cho mã nguồn trở nên phức tạp và khó quản lý.

  • Trước khi áp dụng SRP: Class có nhiều trách nhiệm
Python
class OrderManagement:
    def take_order(self, order):
        # Logic để xử lý đơn hàng
        print("Order taken.")

    def calculate_bill(self, order):
        # Logic để tính toán hóa đơn
        total = sum(item['price'] * item['quantity'] for item in order)
        print(f"Bill calculated: ${total}")
        return total

    def generate_invoice(self, order, total):
        # Logic để tạo file PDF hóa đơn
        print("Invoice generated in PDF format.")

# Sử dụng lớp OrderManagement
order = [
    {"name": "Item 1", "price": 10, "quantity": 2},
    {"name": "Item 2", "price": 20, "quantity": 1}
]

order_management = OrderManagement()
order_management.take_order(order)
total = order_management.calculate_bill(order)
order_management.generate_invoice(order, total)
  • Sau khi áp dụng SRP: Mỗi class mới chỉ đảm nhận một nhiệm vụ duy nhất: OrderProcessor xử lý đơn hàng, BillingCalculator tính toán hóa đơn, và InvoiceGenerator tạo file PDF hóa đơn. Điều này giúp mã nguồn trở nên đơn giản hơn, dễ bảo trì và mở rộng hơn.
Python
class OrderProcessor:
    def take_order(self, order):
        # Logic để xử lý đơn hàng
        print("Order taken.")

class BillingCalculator:
    def calculate_bill(self, order):
        # Logic để tính toán hóa đơn
        total = sum(item['price'] * item['quantity'] for item in order)
        print(f"Bill calculated: ${total}")
        return total

class InvoiceGenerator:
    def generate_invoice(self, order, total):
        # Logic để tạo file PDF hóa đơn
        print("Invoice generated in PDF format.")

# Sử dụng các lớp đã tách riêng
order = [
    {"name": "Item 1", "price": 10, "quantity": 2},
    {"name": "Item 2", "price": 20, "quantity": 1}
]

order_processor = OrderProcessor()
billing_calculator = BillingCalculator()
invoice_generator = InvoiceGenerator()

order_processor.take_order(order)
total = billing_calculator.calculate_bill(order)
invoice_generator.generate_invoice(order, total)

Một ví dụ dễ hiểu trong đời sống về nguyên tắt SRP

💡
Hãy tưởng tượng bạn đang xây một ngôi nhà. Nếu bạn yêu cầu một người thợ xây vừa làm thợ nề, thợ điện, thợ ống nước và thợ sơn, thì khi bạn muốn sửa lại đường ống nước, bạn sẽ phải gọi người này và có nguy cơ ảnh hưởng đến các công việc khác mà người đó đã làm.

Thay vào đó, nếu bạn có một người chuyên làm thợ nề, một người chuyên làm thợ điện, một người chuyên làm thợ ống nước, và một người chuyên sơn, thì khi bạn muốn sửa đường ống nước, bạn chỉ cần gọi thợ ống nước mà không làm ảnh hưởng đến các phần việc khác của ngôi nhà. Điều này giúp bạn dễ dàng quản lý và sửa chữa.

2.2 Open/Closed Principle (OCP)

Open/Closed Principle (OCP) đề cập đến cách thiết kế phần mềm sao cho nó có thể mở rộng bằng cách thêm các tính năng mới mà không cần phải sửa đổi mã nguồn hiện có. Điều này giúp cho mã nguồn ổn định hơn, tránh việc sửa đổi dẫn đến lỗi không mong muốn.

Giả sử bạn đang xây dựng một hệ thống quản lý đơn hàng. Ban đầu, hệ thống chỉ hỗ trợ hai phương thức thanh toán là Thanh toán bằng thẻ tín dụng và Thanh toán bằng PayPal. Sau một thời gian, bạn muốn thêm một phương thức thanh toán mới, chẳng hạn Thanh toán bằng ví điện tử (eWallet).

  • Trước khi áp dụng OCP: Mọi thay đổi sẽ yêu cầu bạn chỉnh sửa mã nguồn hiện có, điều này có thể làm cho hệ thống dễ bị lỗi khi mở rộng.
Python
class Order:
    def __init__(self, amount):
        self.amount = amount

    def process_payment(self, payment_method):
        if payment_method == "credit_card":
            self.pay_by_credit_card()
        elif payment_method == "paypal":
            self.pay_by_paypal()

    def pay_by_credit_card(self):
        print(f"Processing credit card payment of ${self.amount}")

    def pay_by_paypal(self):
        print(f"Processing PayPal payment of ${self.amount}")

# Sử dụng hệ thống thanh toán
order = Order(100)
order.process_payment("credit_card")
order.process_payment("paypal")
  • Sau khi áp dụng OCP: Bạn có thể thiết kế hệ thống sao cho việc thêm phương thức thanh toán mới không yêu cầu phải thay đổi mã nguồn hiện có. Bạn có thể thực hiện điều này bằng cách sử dụng các lớp và kế thừa (inheritance).
Python
from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCardPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Processing credit card payment of ${amount}")

class PayPalPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Processing PayPal payment of ${amount}")

class EWalletPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Processing eWallet payment of ${amount}")

class Order:
    def __init__(self, amount):
        self.amount = amount

    def process_payment(self, payment_processor: PaymentProcessor):
        payment_processor.process_payment(self.amount)

# Sử dụng hệ thống thanh toán với khả năng mở rộng
order = Order(100)
order.process_payment(CreditCardPayment())
order.process_payment(PayPalPayment())

# Thêm phương thức thanh toán mới mà không thay đổi mã cũ
order.process_payment(EWalletPayment())

Một ví dụ dễ hiểu khác về nguyên lý OPC

💡
Hãy tưởng tượng bạn đang thiết kế một chiếc xe hơi. Ban đầu, xe của bạn chỉ có một loại động cơ xăng. Sau đó, bạn muốn thêm tùy chọn động cơ điện. Nếu bạn phải tháo rời và thay đổi toàn bộ xe để thêm động cơ điện, thì điều này sẽ rất phức tạp và tốn kém.

Thay vào đó, nếu từ đầu bạn thiết kế xe sao cho động cơ có thể dễ dàng thay thế và mở rộng, việc thay động cơ mới không làm thay đổi thiết kế cơ bản của chiếc xe, tức là bạn đang tuân thủ nguyên lý OPC.

2.3 Liskov Substitution Principle (LSP)

Nguyên lý Liskov Substitution Principle (LSP) được đặt tên theo nhà khoa học máy tính Barbara Liskov và nó nói rằng: Các đối tượng của một lớp con phải có thể thay thế cho các đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình.

Giả sử bạn đang phát triển một hệ thống quản lý đơn hàng, trong đó có một lớp tên Order để xử lý các đơn hàng. Bạn quyết định tạo ra một lớp con OnlineOrder để xử lý các đơn hàng trực tuyến và lớp InStoreOrder để xử lý các đơn hàng tại cửa hàng.

  • Vi phạm nguyên lý LSP: Bạn có lớp Order với một phương thức process_payment(), và bạn muốn mỗi loại đơn hàng có cách xử lý thanh toán khác nhau. Khi bạn gọi handle_order() với một đối tượng InStoreOrder và phương thức thanh toán không được hỗ trợ (như PayPal), hệ thống sẽ trả ra một ngoại lệ (exception) => Lớp con (OnlineOrderInStoreOrder) không hoàn toàn thay thế được cho lớp cha Order một cách đúng đắn, gây ra các lỗi tiềm ẩn.
Python
class Order:
    def process_payment(self, payment_method):
        # Xử lý thanh toán mặc định
        print(f"Processing payment using {payment_method}")

class OnlineOrder(Order):
    def process_payment(self, payment_method):
        # Xử lý thanh toán trực tuyến
        if payment_method == "credit_card":
            print("Processing online payment with credit card")
        else:
            raise NotImplementedError("Only credit card payments are supported online")

class InStoreOrder(Order):
    def process_payment(self, payment_method):
        # Xử lý thanh toán tại cửa hàng
        if payment_method in ["cash", "credit_card"]:
            print(f"Processing in-store payment with {payment_method}")
        else:
            raise NotImplementedError("Only cash or credit card payments are supported in-store")

# Sử dụng các đối tượng
def handle_order(order: Order, payment_method):
    order.process_payment(payment_method)

online_order = OnlineOrder()
in_store_order = InStoreOrder()

handle_order(online_order, "credit_card")  # Hoạt động bình thường
handle_order(in_store_order, "cash")       # Hoạt động bình thường
handle_order(in_store_order, "paypal")     # Sẽ lỗi vì không hỗ trợ PayPal
  • Tuân theo nguyên lý LSP: Để tuân thủ LSP, bạn nên đảm bảo rằng các lớp con (OnlineOrder, InStoreOrder) có thể thay thế cho lớp cha (Order) mà không làm thay đổi tính đúng đắn của hệ thống. Một cách để làm điều này là tách logic thanh toán thành các lớp riêng biệt và đảm bảo các lớp con có thể xử lý tất cả các phương thức thanh toán một cách phù hợp.
Python
from abc import ABC, abstractmethod

# Lớp cơ sở trừu tượng cho phương thức thanh toán
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCardPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Processing credit card payment of ${amount}")

class CashPayment(PaymentProcessor):
    def process_payment(self, amount):
        print(f"Processing cash payment of ${amount}")

# Lớp Order với các phương thức thanh toán được truyền vào
class Order(ABC):
    def __init__(self, amount, payment_processor: PaymentProcessor):
        self.amount = amount
        self.payment_processor = payment_processor

    def process_order(self):
        self.payment_processor.process_payment(self.amount)

class OnlineOrder(Order):
    pass

class InStoreOrder(Order):
    pass

# Sử dụng các đối tượng
def handle_order(order: Order):
    order.process_order()

online_order = OnlineOrder(100, CreditCardPayment())
in_store_order = InStoreOrder(50, CashPayment())

handle_order(online_order)  # Xử lý thanh toán trực tuyến
handle_order(in_store_order) # Xử lý thanh toán tại cửa hàng

Một ví dụ dễ hiểu trong đời sống về nguyên lý LSP

💡
Hãy tưởng tượng bạn có một chiếc xe đạp. Bạn thường sử dụng nó để đi từ nhà đến trường. Một ngày, bạn quyết định nâng cấp lên một chiếc xe đạp điện. Bạn mong đợi rằng dù có thay đổi phương tiện, bạn vẫn có thể sử dụng nó để đi từ nhà đến trường mà không gặp trở ngại nào.

Tương tự, trong lập trình, nếu bạn thay thế một đối tượng của lớp cha bằng một đối tượng của lớp con, chương trình vẫn nên hoạt động bình thường, đúng như cách nó đã làm với lớp cha. Nếu không, bạn đã vi phạm nguyên lý Liskov.

2.4 Interface Segregation Principle (ISP)

Interface Segregation Principle (ISP) là một nguyên lý giúp đảm bảo rằng các lớp không bị ép buộc phải triển khai các phương thức mà chúng không sử dụng. Nói cách khác, thay vì tạo ra một giao diện lớn (fat interface) với nhiều phương thức mà không phải tất cả các lớp đều cần, bạn nên tách nó ra thành các interface nhỏ hơn, mỗi interface chỉ có những phương thức liên quan đến một nhóm nhiệm vụ cụ thể.

Giả sử bạn có một hệ thống quản lý đơn hàng, trong đó có một interface OrderOperations với các phương thức như place_order(), cancel_order(), track_order(), và refund_order(). Nhưng không phải tất cả các loại đơn hàng đều cần tất cả các phương thức này. Ví dụ, đơn hàng tại cửa hàng (in-store order) có thể không cần phương thức track_order()refund_order().

  • Vi phạm nguyên lý ISP: Nếu bạn ép buộc tất cả các loại đơn hàng phải triển khai toàn bộ interface OrderOperations, thì bạn sẽ vi phạm nguyên lý ISP. Lớp InStoreOrder phải triển khai các phương thức track_order()refund_order(), mặc dù nó không thực sự cần đến chúng.
Python
class OrderOperations:
    def place_order(self):
        raise NotImplementedError
    
    def cancel_order(self):
        raise NotImplementedError
    
    def track_order(self):
        raise NotImplementedError
    
    def refund_order(self):
        raise NotImplementedError

class InStoreOrder(OrderOperations):
    def place_order(self):
        print("Placing in-store order")
    
    def cancel_order(self):
        print("Cancelling in-store order")
    
    def track_order(self):
        pass  # Không thực sự cần thiết nhưng vẫn phải triển khai
    
    def refund_order(self):
        pass  # Không thực sự cần thiết nhưng vẫn phải triển khai

class OnlineOrder(OrderOperations):
    def place_order(self):
        print("Placing online order")
    
    def cancel_order(self):
        print("Cancelling online order")
    
    def track_order(self):
        print("Tracking online order")
    
    def refund_order(self):
        print("Refunding online order")

# Sử dụng các lớp
in_store_order = InStoreOrder()
in_store_order.place_order()
in_store_order.track_order()  # Gọi phương thức nhưng không có gì xảy ra

online_order = OnlineOrder()
online_order.place_order()
online_order.track_order()  # Thực hiện đúng chức năng
  • Tuân theo nguyên lý ISP: Để tuân theo ISP, bạn nên tách giao diện OrderOperations thành các giao diện nhỏ hơn, chẳng hạn như Placeable, Cancelable, Trackable, và Refundable. Mỗi lớp chỉ cần triển khai những giao diện mà nó thực sự cần.
Python
from abc import ABC, abstractmethod

class Placeable(ABC):
    @abstractmethod
    def place_order(self):
        pass

class Cancelable(ABC):
    @abstractmethod
    def cancel_order(self):
        pass

class Trackable(ABC):
    @abstractmethod
    def track_order(self):
        pass

class Refundable(ABC):
    @abstractmethod
    def refund_order(self):
        pass

class InStoreOrder(Placeable, Cancelable):
    def place_order(self):
        print("Placing in-store order")
    
    def cancel_order(self):
        print("Cancelling in-store order")

class OnlineOrder(Placeable, Cancelable, Trackable, Refundable):
    def place_order(self):
        print("Placing online order")
    
    def cancel_order(self):
        print("Cancelling online order")
    
    def track_order(self):
        print("Tracking online order")
    
    def refund_order(self):
        print("Refunding online order")

# Sử dụng các lớp
in_store_order = InStoreOrder()
in_store_order.place_order()

online_order = OnlineOrder()
online_order.place_order()
online_order.track_order()  # Thực hiện đúng theo nhu cầu

Một ví dụ dễ hiểu trong đời sống về nguyên lý ISP

💡
Hãy tưởng tượng bạn đang làm việc trong một công ty, và công việc của bạn là xử lý đơn hàng. Tuy nhiên, công ty lại yêu cầu bạn phải biết cả cách quản lý tài chính, điều hành nhân sự, và phát triển sản phẩm - những nhiệm vụ không liên quan đến công việc chính. Điều này sẽ làm bạn cảm thấy quá tải và mất tập trung vào nhiệm vụ chính của mình.

Tương tự, trong lập trình, nếu một lớp phải triển khai quá nhiều phương thức mà không liên quan đến nhiệm vụ chính của nó, mã nguồn sẽ trở nên rối rắm và khó bảo trì.

2.5 Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP) là nguyên lý cuối cùng trong SOLID, nó nhấn mạnh việc giảm sự phụ thuộc giữa các module cấp cao và module cấp thấp trong hệ thống bằng cách sử dụng các abstraction (lớp trừu tượng hoặc interface) thay vì các concretion (cụ thể, chi tiết triển khai).

Giả sử bạn có một hệ thống quản lý đơn hàng, trong đó có một lớp OrderProcessor để xử lý đơn hàng và một lớp EmailNotifier để gửi thông báo cho khách hàng khi đơn hàng được xử lý.

  • Trước khi áp dụng DIP: OrderProcessor phụ thuộc trực tiếp vào EmailNotifier. Nếu bạn muốn thay đổi cách gửi thông báo (ví dụ: chuyển từ email sang SMS), bạn sẽ phải sửa đổi lớp OrderProcessor, làm cho mã nguồn trở nên khó bảo trì và mở rộng.
Python
class EmailNotifier:
    def send_email(self, message):
        print(f"Sending email: {message}")

class OrderProcessor:
    def __init__(self, order_id):
        self.order_id = order_id
        self.notifier = EmailNotifier()  # Phụ thuộc trực tiếp vào EmailNotifier

    def process_order(self):
        print(f"Processing order {self.order_id}")
        self.notifier.send_email(f"Order {self.order_id} has been processed")

# Sử dụng hệ thống
order_processor = OrderProcessor(123)
order_processor.process_order()
  • Sau khi áp dụng DIP: Để tuân theo nguyên lý DIP, bạn có thể tạo một abstraction (interface Notifier) và để OrderProcessor phụ thuộc vào abstraction này thay vì phụ thuộc trực tiếp vào EmailNotifier.
Python
from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def notify(self, message):
        pass

class EmailNotifier(Notifier):
    def notify(self, message):
        print(f"Sending email: {message}")

class SMSNotifier(Notifier):
    def notify(self, message):
        print(f"Sending SMS: {message}")

class OrderProcessor:
    def __init__(self, order_id, notifier: Notifier):
        self.order_id = order_id
        self.notifier = notifier  # Phụ thuộc vào abstraction Notifier

    def process_order(self):
        print(f"Processing order {self.order_id}")
        self.notifier.notify(f"Order {self.order_id} has been processed")

email_notifier = EmailNotifier()
sms_notifier = SMSNotifier()

order_processor_email = OrderProcessor(123, email_notifier)
order_processor_email.process_order()

order_processor_sms = OrderProcessor(456, sms_notifier)
order_processor_sms.process_order()

Một ví dụ dễ hiểu trong đời sống về nguyên lý ISP

💡
Hãy tưởng tượng bạn đang xây dựng một ngôi nhà. Bạn có một hệ thống điện (module cấp cao) và nhiều loại bóng đèn khác nhau (module cấp thấp). Nếu hệ thống điện chỉ hoạt động với một loại bóng đèn cụ thể, thì mỗi khi bạn muốn đổi loại bóng đèn mới, bạn sẽ phải thay đổi toàn bộ hệ thống điện trong nhà.

Thay vào đó, nếu hệ thống điện được thiết kế để hoạt động với bất kỳ bóng đèn nào (bằng cách sử dụng một loại ổ cắm chuẩn - abstraction), bạn có thể dễ dàng thay đổi bóng đèn mà không cần lo lắng về hệ thống điện. Đây chính là ý tưởng của DIP: module cấp cao chỉ nên phụ thuộc vào abstraction, không phụ thuộc vào các chi tiết cụ thể.

3. Kết luận

Từng nguyên lý trong SOLID - từ SRP, OCP, LSP, ISP, đến DIP - đều hướng đến việc tạo ra mã nguồn rõ ràng, ít lỗi và linh hoạt hơn. Khi được áp dụng đúng cách, SOLID không chỉ cải thiện chất lượng mã nguồn mà còn giúp các dự án phát triển phần mềm dễ dàng thích nghi với những thay đổi trong tương lai, từ đó giảm thiểu rủi ro và tăng hiệu quả làm việc của nhóm phát triển.

Các bài viết liên quan tại Blog 200Lab:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab