Nguyên tắc SOLID trong lập trình hướng đối tượng (OOP) – thực hành cùng ngôn ngữ Python

SOLID là gì?

SOLID là 5 nguyên tắc nền tảng trong lập trình hướng đối tượng OOP (Object Oriented Programming), giúp thiết kế chương trình hướng đối tượng linh động, dễ hiểu và dễ duy trì.

Là viết tắt của 5 chữ cái đầu của mỗi nguyên tắc sau:

            S: Single Responsibility Principle (SRP)

            O: Open-Closed principle (OCP)

            L: Liskov Substitution Principle (LSP)

            I: Interface Segregation Principle (ISP)

            D: Dependency Inversion Principle (DIP)

SOLID theo hướng thực hành cùng Python

Bài viết này sử dụng một chương trình Python cơ bản, lần lượt đi qua từng nguyên tắc trong SOLID, chia làm 3 bước:

1. Nội dung của nguyên tắc

2. Phân tích lý do vì sao lại vi phạm nguyên tắc

3. Cải thiện chương trình để không vi phạm nguyên tắc nữa

Qua đó, giúp cải thiện kỹ năng lập trình hướng đối tượng của bạn.

Giới thiệu chương trình:

Ứng dụng mua hàng, có lớp Order (đơn hàng) chứa các thuộc tính và phương thức sau.

Các thuộc tính: items (hàng hóa), quantities (số lượng), prices (giá cả), status (trạng thái)

Các phương thức:

– add_item: thêm một sản phẩm gồm name, quantity, price vào đơn hàng

– pay: tiến hành thanh toán với phương thức thanh toán và mã bảo mật

class Order:
    items = []
    quantities = []
    prices = []
    status = "open"

    def add_item(self, name, quantity, price):
        self.items.append(name)
        self.quantities.append(quantity)
        self.prices.append(price)

    def total_price(self):
        total = 0
        for i in range(len(self.prices)):
            total += self.quantities[i] * self.prices[i]
        
        return total
    
    def pay(self, payment_type, 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}")

Thực hiện chương trình:

– Tạo đơn hàng, thêm các sản phẩm vào giỏ hàng.

– In ra giá tiền của đơn hàng và thanh toán

order = Order()
order.add_item("Macbook Pro", 1, 600)
order.add_item("Keyboard", 1, 100)
order.add_item("iphone X", 1, 400)

print(order.total_price())
order.pay("debit", "13454356")

Kết quả:

1100
Processing debit payment type
Verifying security code: 13454356

Nguyên tắc S: Single responsibility principle

Nội dung nguyên tắc

Từ Wikipedia:

The Single-responsibility principle:

“There should never be more than one reason for a class to change.” In other words, every class should have only one responsibility.

Tạm dịch: Mỗi lớp đối tượng chỉ nên có một lý do để thay đổi. Đồng nghĩa với, mỗi lớp đối tưởng chỉ nên có một trách nhiệm duy nhất.

Nguyên tắc này chỉ ra mỗi lớp hay phương thức chỉ nên có một trách nhiệm duy nhất.

Nhờ đó, khi thay đổi chúng cũng được thay đổi với một mục đích nhất định, và có thể dễ dàng sử dụng lại ở nhiều nơi.

Thực hành

Giải thích lý do vi phạm

Quan sát ví dụ trên, class Order chứa phương thức add_item, tính total_price là hợp lý vì cả hai đều liên quan đến đơn hàng. 

Nhưng pay thì nên tách ra riêng biệt vì phụ thuộc vào các yếu tố không thuộc về order như các loại phương thức thanh toán payment_type.

Nên đoạn code này vi phạm nguyên tắc SRP.

Cải thiện

Để cải thiện, tách riêng payment ra một lớp riêng biệt gọi là PaymentProcesser. 

Lớp này có các phương thức thanh toán là từng phương thức riêng biệt.

Đồng thời xóa phương thức pay ở Order.

Code sẽ thay đổi như sau:

# ...

class PaymentProcessor:
    def pay_credit(self, order, security_code):
        print("Processing debit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"
        
    def pay_debit(self, order, security_code):
        print("Processing credit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"
        
# ...
print(order.total_price())
processor = PaymentProcessor()
processor.pay_debit(order, "13454356")

Và thay vì gọi order.pay(…) như cũ thì sẽ tạo một đối tượng cho phương thức thanh toán và gọi đến phương thức thanh toán pay_debit.

Kết quả vẫn vậy, nhưng code đã không còn vi phạm nguyên tắc S nữa. 

Nguyên tắc O - Open-closed principle

Nội dung nguyên tắc

Từ Wikipedia:

The Open–closed principle: 
Software entities … should be open for extension, but closed for modification.

Tạm dịch: Các thực thể phần mềm chỉ nên mở rộng để kế thừa và không nên chỉnh sửa.

Nguyên tắc này chỉ ra các thực thể phần mềm như lớp đối tượng khuyến khích mở rộng ra bằng cách kế thừa chứ không nên chỉnh sửa code đã có sẵn.

Thực hành

Giải thích lý do vi phạm

Cũng với ví dụ trên, nếu một phương thức thanh toán mới được thêm vào, ví dụ như paypal, thì cần phải thêm một phương thức mới vào lớp PaymentProcessor. 

Điều này sẽ vi phạm nguyên tắc OCP này vì đã chỉnh sửa lớp PaymentProcessor.

Cải thiện

Để xử lý mà không vi phạm nguyên tắc OCP, có thể tạo thêm lớp trừu tượng PaymentProccessor với phương thức pay là một abstractmethod. 

Và sử dụng lớp này để mở rộng ra tạo các lớp cụ thể hơn cho từng loại phương thức thanh toán như DebitPaymentProcessor và viết phương thức pay riêng của mình.

from abc import ABC, abstractmethod

# ...
class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order, security_code):
        pass

class DebitPaymentProcessor(PaymentProcessor):
    def pay(self, order, security_code):
        print("Processing debit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"

class CreditPaymentProcessor(PaymentProcessor):
    def pay(self, order, security_code):
        print("Processing credit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"
        
# ...

print(order.total_price())
processor = DebitPaymentProcessor()
processor.pay(order, "13454356")

Thiết kế như vậy thì khi sử dụng sẽ thay đổi thành tạo DebitPaymentProcessor() và gọi phương thức pay của nó.

Đồng thời với phương thức thanh toán mới, paypal, có thể mở rộng từ PaymentProcessor với tên PaypalPaymentProcessor. 

Đồng thời viết phương thức pay riêng cho paypal mà không thay đổi code có sẵn. 

Như vậy sẽ không phạm vào nguyên tắc OCP nữa.

class PaypalPaymentProcessor(PaymentProcessor):
    def pay(self, order, security_code):
        print("Processing credit payment type")
        print(f"Verifying security code: {security_code}")
        order.status = "paid"

Nguyên tắc L - Liskov substitution principle

Nội dung nguyên tắc

Từ Wikipedia:

The Liskov substitution principle:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Tạm dịch: Các đối tượng hay phương thức của lớp con (lớp dẫn xuất) có thể sử dụng thay thế đối tượng hay phương thức của lớp cha mà không có lỗi.

Nguyên tắc này chỉ ra một lớp con nên sử dụng thay thế lớp cha mà không làm trái ý nghĩa của lớp cha hay thậm chí gây lỗi.

Thực hành

Giải thích lý do vi phạm

Giả sử với phương thức thanh toán Paypal, cách xử lý thanh toán có khác với các loại thẻ, thay vì security_code lại cần dùng email.

Khi đó, giá trị email sẽ được truyền vào vào giá trị của biến security_code như thế này:

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order, security_code):
        pass
        
class PaypalPaymentProcessor(PaymentProcessor):
    def pay(self, order, security_code):
        print("Processing credit payment type")
        print(f"Verifying email address: {security_code}")
        order.status = "paid"
        

order = Order()
order.add_item("Macbook Pro", 1, 600)
order.add_item("Keyboard", 1, 100)
order.add_item("iphone X", 1, 400)

print(order.total_price())
processor = DebitPaymentProcessor()
processor.pay(order, "user@domain.com")

Tuy nhiên với cách này thì security_code của paypal sẽ không có ý nghĩa mà lớp cha PaymentProcessor mong đợi. 

Điều này sẽ làm vi phạm nguyên tắc LSP vì lớp con PaypalPaymentProcessor sử dụng biến security_code như là email không có cùng ý nghĩa với lớp cha là mã bảo mật.

Cải thiện

Để xử lý không vi phạm nguyên tắc LSP, tại đây cần xóa security_code ra khỏi lớp cha PaymentProcessor. Đồng thời, chuyển đổi bạn ấy vào biến khởi tạo cho từng lớp con cần bạn là CreditPaymentProcessor và DebitPaymentProcessor.

Còn lớp PaypalPaymentProcessor, khởi tạo với biến email_address dành riêng cho phương thức thanh toán này.

Code sẽ thay đổi như bên dưới:

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

class DebitPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def pay(self, order):
        print("Processing debit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

class CreditPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def pay(self, order):
        print("Processing credit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"
        
class PaypalPaymentProcessor(PaymentProcessor):
    def __init__(self, email_address):
        self.email_address = email_address

    def pay(self, order):
        print("Processing credit payment type")
        print(f"Verifying email address: {self.email_address}")
        order.status = "paid"
        

order = Order()
order.add_item("Macbook Pro", 1, 600)
order.add_item("Keyboard", 1, 100)
order.add_item("iphone X", 1, 400)

print(order.total_price())
processor = PaypalPaymentProcessor("user@domain.com")
processor.pay(order)

Sau khi xử lý như vậy, có thể thấy lớp con không làm thay đổi ngữ nghĩa của lớp cha nữa. Do đó không còn phạm nguyên tắc LSP.

Nguyên tắc I - Interface segregation principle

Nội dung nguyên tắc

Từ Wikipedia:

The Interface segregation principle: 

Clients should not be forced to depend upon interfaces that they do not use.

Tạm dịch: Một lớp con (lớp trích xuất) không nên bắt buộc kế thừa những phương thức interface mà nó không sử dụng. Nên tách nhỏ ra nhiều interface để kế thừa đúng cái cần mà thôi.

Nguyên tắc này chỉ ra rằng nếu một lớp con được trích xuất từ lớp cha nhưng không sử dụng hết các phương thức của lớp cha. Hay các phương thức này lớp con bắt buộc phải kế thừa nhưng bị thừa. 

Thì đây, chính là lúc bạn cần tách ra nhiều interface với mục đích cụ thể khác nhau để lớp con chỉ kế thừa những cái bạn cần, thay vì một interface chung lớn.

Thực hành

Giải thích lý do vi phạm

Giả sử có thêm phương thức xác minh qua tin nhắn, auth_sms trong lớp PaymentProcessor chịu trách nhiệm nhân vào mã code và tiến hành xác minh danh tính. 

Nếu đã xác minh, verified = True thì cho phép tiến hành thanh toán. Và ngược lại, sẽ báo lỗi chưa được xác minh.

Tuy nhiên, xác minh SMS lại không áp dụng cho phương thức thanh toán credit card. Do đó, sẽ báo lỗi là không hỗ trợ xác minh SMS với credit card payment.

Đoạn code cho tình huống này như sau:

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

    @abstractmethod
    def auth_sms(self, code):
        pass

    

class DebitPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def auth_sms(self, code):
        print(f"Verifying SMS code {code}")
        self.verified = True

    def pay(self, order):
        if not self.verified:
            raise Exception("Not authorized")
        print("Processing debit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

class CreditPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def auth_sms(self, code):
        raise Exception("Credit card payments don't support SMS code authorization.")

    def pay(self, order):
        print("Processing credit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

Lớp PaypalPaymentProcessor tương tự với DebitPaymentProcessor nên mình sẽ tạm không ghi lại.

Trong tình huống này, thì lớp CreditPaymentProcessor đã không cần sử dụng auth_sms, cái được kế thừa từ lớp cha PaymentProcessor. 

Do đó, đoạn code trên vi phạm nguyên tắc ISP.

Cải thiện

Khi lớp con không dùng đến phương thức của lớp cha nữa, thì chính là lúc nên tách lớp cha ra nhiều interface khác nhau.

Tạo một lớp mới trích xuất từ PaymentProcessor với tên PaymentProcessor_SMS và chuyển phương thức auth_sms vào đây.

Lớp DebitPaymentProcessor và lớp PaypalPaymentProcessor sẽ được kế thừa từ PaymentProcessor_SMS vì cần sử dụng auth_sms. 

Còn CreditPaymentProcessor sẽ không cần xử lý auth_sms vì nó không còn ở lớp PaymentProcessor nữa.

Code sẽ thay đổi thành:

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

class PaymentProcessor_SMS(PaymentProcessor):
    @abstractmethod
    def auth_sms(self, code):
        pass

class DebitPaymentProcessor(PaymentProcessor_SMS):
    def __init__(self, security_code):
        self.security_code = security_code
        self.verified = False

    def auth_sms(self, code):
        print(f"Verifying SMS code {code}")
        self.verified = True

    def pay(self, order):
        if not self.verified:
            raise Exception("Not authorized")
        print("Processing debit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

class CreditPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def pay(self, order):
        print("Processing credit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

class PaypalPaymentProcessor(PaymentProcessor_SMS):
    def __init__(self, email_address):
        self.email_address = email_address
        self.verified = False

    def auth_sms(self, code):
        print(f"Verifying SMS code {code}")
        self.verified = True
        
    def pay(self, order):
        if not self.verified:
            raise Exception("Not authorized")
        print("Processing credit payment type")
        print(f"Verifying email address: {self.email_address}")
        order.status = "paid"

Code trên đã sạch hơn, tuy nhiên việc xác minh SMS vẫn đang thuộc quá trình payment. 

Có thể áp dụng composition để tách quá trình này ra thêm một interface riêng chỉ dùng để xác thực, đặt tên class mới là Auth_SMS thực hiện quá trình xác thực này riêng biệt.

Sau đó từng lớp payment cụ thể sẽ được truyền vào một authorizer tương ứng giúp xác thực.

class PaymentProcessor(ABC):
    @abstractmethod
    def pay(self, order):
        pass

class SMS_Auth:
    authorized = False

    def verify_code(self, code):
        print(f"Verifying code {code}")
        self.authorized = True

    def is_authorized(self) -> bool:
        return self.authorized


class DebitPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code, authorizer: SMS_Auth):
        self.security_code = security_code
        self.authorizer = authorizer

    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        print("Processing debit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"

class CreditPaymentProcessor(PaymentProcessor):
    def __init__(self, security_code):
        self.security_code = security_code

    def pay(self, order):
        print("Processing credit payment type")
        print(f"Verifying security code: {self.security_code}")
        order.status = "paid"


class PaypalPaymentProcessor(PaymentProcessor):
    def __init__(self, email_address, authorizer):
        self.email_address = email_address
        self.authorizer = authorizer
        
    def pay(self, order):
        if not self.authorizer.is_authorized():
            raise Exception("Not authorized")
        print("Processing credit payment type")
        print(f"Verifying email address: {self.email_address}")
        order.status = "paid"
        

order = Order()
order.add_item("Macbook Pro", 1, 600)
order.add_item("Keyboard", 1, 100)
order.add_item("iphone X", 1, 400)

print(order.total_price())
authorizer = SMS_Auth()
processor = PaypalPaymentProcessor("user@domain.com", authorizer)
processor.pay(order)

Cách xử lý này sẽ tách ra nhiều interface với các mục đích riêng và tái sử dụng ở lớp con. Do đó, không còn vi phạm nguyên tắc ISP nữa.

Nguyên tắc D - Dependency inversion principle

Nội dung nguyên tắc

Từ Wikipedia:

The Dependency inversion principle:

Depend upon abstractions, [not] concretions.

Tạm dịch: Các mô-đun cấp cao không nên phụ thuộc vào module cấp thấp. Nếu có sự phụ thuộc thì cả hai nên phụ thuộc vào abstraction hay interface.

Nguyên tắc này chỉ ra các lớp không nên phụ thuộc lẫn nhau, mà nên xây dựng interface để phụ thuộc.

Thực hành

Giải thích lý do vi phạm

Giả sử có thêm một phương thức authorizer nữa là xác minh không phải là robot đang sử dụng (có thể bằng capcha hoặc chọn hình ảnh liên quan). Thì sẽ cần phải thay đổi authorizer không sử dụng SMS_Auth nữa.

Đoạn code ở trên, bạn có thể thấy lớp PaypalPaymentProcessor đang phụ thuộc vào lớp SMS_Auth

Do đó sẽ vi phạm nguyên tắc DIP.

Cải thiện

Xây dựng một lớp abstract đại diện cho Authorizer với phương thức is_authorized.

Thì với cách xác minh không phải là robot, sẽ tạo một lớp mở rộng từ Authorizer, đặt tên NotARobot_Auth và xác minh theo cách riêng biệt với phương thức not_a_robot.

Đồng thời, authorizer sẽ có loại là Authorizer thay vì SMS_Auth. Thì có thể thích nghi với cả SMS_Auth hay NotARobot_Auth.

Code sẽ thay đổi thành:

class Authorizer(ABC):
    @abstractmethod
    def is_authorized(self, order):
        pass

class SMS_Auth(Authorizer):
    authorized = False

    def verify_code(self, code):
        print(f"Verifying code {code}")
        self.authorized = True

    def is_authorized(self) -> bool:
        return self.authorized

class NotARobot_Auth(Authorizer):
    authorized = False

    def not_a_robot(self):
        print(f"Are you a robot? Naaaa ...")
        self.authorized = True

    def is_authorized(self) -> bool:
        return self.authorized


class PaypalPaymentProcessor(PaymentProcessor): def __init__(self, email_address, authorizer: Authorizer): self.email_address = email_address self.authorizer = authorizer def pay(self, order): if not self.authorizer.is_authorized(): raise Exception("Not authorized") print("Processing credit payment type") print(f"Verifying email address: {self.email_address}") order.status = "paid" # ... print(order.total_price()) authorizer = NotARobot_Auth() processor = PaypalPaymentProcessor("user@domain.com", authorizer) authorizer.not_a_robot() processor.pay(order)

Code trên đã không còn vi phạm nguyên tắc DIP nữa vì cả lớp con như PaypalPaymentProcessor hay lớp SMS_Auth đều không phụ thuộc vào nhau nhưng phụ thuộc vào interface Authorizer.

Lời kết

Nếu bạn đọc đến đây thì là bạn thực sự muốn tìm hiểu về SOLID, cảm ơn bạn ^^

5 nguyên tắc của SOLID cùng các ví dụ thực hành bên trên sẽ giúp bạn lập trình hướng đối tượng hiệu quả hơn, code sạch và dễ mở rộng và bảo trì. 

Khi nắm rõ các nguyên tắc này, hãy bắt tay vào cải thiện từng phần một để nâng cao kỹ năng code nhé.

Các ví dụ trong bài viết này mình sử dụng từ video “Uncle Bob’s SOLID principles made easy 🍀 – in Python!”, bạn có thể ghé xem kênh này có nhiều video về Python rất hay.

Happy reading!

Leave a Reply

Your email address will not be published. Required fields are marked *

RELATED POST

Web Developer Learning Resources

Developer Roadmap roadmap.sh is a community effort to create roadmaps, guides and other educational content to help guide developers in…

Git

Bài này note lại vài tài liệu hay về git. How Git Commands work? https://www.youtube.com/watch?v=e9lnsKot_SQ Git Merge vs. Git Rebase…

Relation fields in Django Rest Framework Serializer

The Django model offers various types of relationships such as OneToOneField, ForeignKey, ManyToManyField, and GenericForeignKey.  To present or write data…

Tìm hiểu về DNS

DNS là gì ? Người dùng sử dụng web thông qua các tên miền (domain) của trang web như beautyoncode.com. Các…

%d bloggers like this: