1. SOLID là gì?
SOLID trong lập trình hướng đối tượng là tậ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
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.
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
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.
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).
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
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ứcprocess_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ọihandle_order()
với một đối tượngInStoreOrder
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 (OnlineOrder
vàInStoreOrder
) không hoàn toàn thay thế được cho lớp chaOrder
một cách đúng đắn, gây ra các lỗi tiềm ẩn.
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.
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
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()
và 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ớpInStoreOrder
phải triển khai các phương thứctrack_order()
vàrefund_order()
, mặc dù nó không thực sự cần đến chúng.
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.
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
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àoEmailNotifier
. 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ớpOrderProcessor
, làm cho mã nguồn trở nên khó bảo trì và mở rộng.
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àoEmailNotifier
.
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
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: