什麼是 SOLID 原則?
2023年3月7日
什麼是 SOLID 原則?
SOLID 原則是一組軟體設計原則,用於指導軟體開發人員設計和實現高質量的、易於維護和擴展的軟體。它是由羅伯特·C·馬丁在其著作《Agile Software Development, Principles, Patterns, and Practices》中提出的,是目前軟體工程界被廣泛接受的一種軟體設計理念。
SOLID 五個原則
SOLID 原則包括以下五個原則:
- 單一職責原則(Single Responsibility Principle,SRP)
一個類別只應該有一個職責。也就是說,一個類別應該只有一個引起它變化的原因。以下範例表示,
class ShoppingCart:
def __init__(self):
self.items = []
self.total = 0
def add_item(self, item):
self.items.append(item)
self.total += item.price
def remove_item(self, item):
self.items.remove(item)
self.total -= item.price
def print_receipt(self):
print('Items:')
for item in self.items:
print(f'{item.name} - ${item.price}')
print(f'Total: ${self.total}')
這個 ShoppingCart
類別同時負責處理購物車相關的任務和輸出相關的任務。它的print_receipt()
方法應該被拆分為一個獨立的類別或方法,以實現單一職責原則。
- 開放封閉原則(Open-Closed Principle,OCP)
軟體實體(類別、模組、函數等)應該對擴展開放,對修改封閉。這意味著當需要添加新功能時,應該擴展現有的實體,而不是修改它們。
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return total_price
class DiscountedShoppingCart(ShoppingCart):
def get_total_price(self):
total_price = super().get_total_price()
return total_price * 0.9
在這個範例中,當我們需要新增一個帶有不同折扣的購物車時,我們必須創建一個新的子類別 DiscountedShoppingCart
並重寫 get_total_price()
方法,而這也違反了開放封閉原則。
以下是一個符合開放封閉原則的修正過後的 Python 程式碼範例:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return total_price
class Discount:
def calculate_discount(self, total_price):
return total_price
class TenPercentDiscount(Discount):
def calculate_discount(self, total_price):
return total_price * 0.9
class ShoppingCartWithDiscount:
def __init__(self, discount: Discount):
self.items = []
self.discount = discount
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
self.items.remove(item)
def get_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return self.discount.calculate_discount(total_price)
在這個修正過後的範例中,我們將 get_total_price()
****方法重構為使用 Discount
策略類別,而 Discount
****策略類別定義了一個共通的介面,可以用來擴展任何類型的折扣。TenPercentDiscount
是 Discount
的一個子類別,它實現了 10% 的折扣計算。ShoppingCartWithDiscount
類別持有一個 Discount
實例,並在計算總價時調用 calculate_discount()
方法。這樣,當我們需要新增一種不同類型的折扣時,只需要創建一個新的策略類別並將其傳遞給 ShoppingCartWithDiscount
即可,而無需修改現有的程式碼。
- 里氏替換原則(Liskov Substitution Principle,LSP)
所有引用基類別的地方必須能夠透明地使用其子類別的對象。換句話說,子類別應該可以替換其父類別並且不會破壞系統的正確性。
下面這個案例違反了 LSP,因為Rectangle
和 Square
類別之間繼承關係是有問題的,因為 Square
繼承自 Rectangle
,但是 Square
的 set_height()
和 set_width()
方法讓它可以更改兩個邊的長度,從而違反了 Rectangle
的定義,因為 Rectangle
的兩邊可以有不同的長度。這導致在使用 Rectangle
類別對象的代碼中,不能正確地使用 Square
對象來代替,進而違反了 LSP。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, size):
self.width = size
self.height = size
def set_width(self, width):
self.width = width
self.height = width
def set_height(self, height):
self.width = height
self.height = height
square = Square(5)
square.set_width(10)
square.set_height(5)
print(square.area())
簡單來說,LSP 要求子類別可以替代父類別在任何情況下使用,如果子類別有新增或修改方法而不被父類別所擁有,這樣就會破壞原本設計的抽象。在這個例子中,Square
的特殊性質和 Rectangle
的不同導致它不能完全替代 Rectangle
,違反了 LSP。簡單來說,LSP 要求子類別可以替代父類別在任何情況下使用,如果子類別有新增或修改方法而不被父類別所擁有,這樣就會破壞原本設計的抽象。在這個例子中,Square
的特殊性質和 Rectangle
的不同導致它不能完全替代 Rectangle
,違反了 LSP。下面再來看看修復過後的程式碼:
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, size):
self.size = size
def set_size(self, size):
self.size = size
def area(self):
return self.size ** 2
def print_area(shape):
print(f"Area: {shape.area()}")
shapes = [Rectangle(5, 10), Square(5)]
for shape in shapes:
print_area(shape)
這個範例中,Rectangle
和 Square
都繼承自 Shape
類別,並且都實作了 area()
方法。在 print_area()
函數中,接受一個 Shape
物件作為引數,然後呼叫 area()
方法取得其面積。
這個範例中並沒有違反 LSP,因為 Square
物件可以成功地替換 Shape
物件,而不影響程式的正常執行。換句話說,Square
物件與 Shape
物件具有相同的行為,且能夠替代 Shape
物件,因此符合里氏替換原則。
- 介面隔離原則(Interface Segregation Principle,ISP)
客戶端不應該被迫依賴於它不使用的接口。接口應該被拆分為更小和更具體的部分,以便客戶端只需要知道它們所需的部分。
先來看一段違反 ISP 的程式碼:
class Machine:
def print(self, document):
pass
def fax(self, document):
pass
def scan(self, document):
pass
class MultiFunctionPrinter(Machine):
def print(self, document):
print("Printing")
def fax(self, document):
print("Faxing")
def scan(self, document):
print("Scanning")
上述程式碼中,Machine
是一個機器的介面,包含了 print
、fax
和 scan
三個方法。而 MultiFunctionPrinter
是一個具有多種功能的印表機,它繼承了 Machine
介面並實現了所有方法。這段程式碼違反了 ISP,因為不是所有的機器都需要實現 fax
和 scan
方法,而 MultiFunctionPrinter
強制實現了這兩個方法,這樣的設計對於其他只需要實現 print
方法的機器來說是多餘的,也讓介面變得不清晰。
再來看一段修復過後的程式碼:
class Printer:
def print(self, document):
pass
class Fax:
def fax(self, document):
pass
class Scanner:
def scan(self, document):
pass
class MultiFunctionDevice(Printer, Fax, Scanner):
def print(self, document):
print("Printing")
def fax(self, document):
print("Faxing")
def scan(self, document):
print("Scanning")
在這個新的設計中,我們將原來的介面 Machine
拆成了三個獨立的介面 Printer
、Fax
和 Scanner
。MultiFunctionDevice
是一個具有多種功能的裝置,它實現了 Printer
、Fax
和 Scanner
三個介面。這樣的設計讓每個介面只包含必要的方法,並讓裝置可以實現自己所需要的介面。這樣的設計更符合 ISP。
- 依賴反轉原則(Dependency Inversion Principle,DIP)
高層模組不應該依賴於低層模組,兩者都應該依賴於抽象。換句話說,高層模組和低層模組之間應該通過介面或抽象類別進行交互。這樣可以減少類別之間的直接耦合,從而提高代碼的靈活性和可重用性。
先來看一個錯誤的範例:
class Logger:
def log(self, message):
print(f"Log: {message}")
class UserService:
def __init__(self):
self.logger = Logger()
def register(self, username, password):
try:
# register user to database
print(f"User {username} registered successfully")
self.logger.log(f"User {username} registered successfully")
except Exception as e:
print(f"Error: {e}")
self.logger.log(f"Error: {e}")
在這個例子中, UserService
直接創建 Logger
物件。這個設計違反了依賴反轉原則,因為 UserService
的高層次模組直接依賴 Logger
這個低層次模組。
以下為修復過後的範例:
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
class ConsoleLogger(Logger):
def log(self, message):
print(f"Log: {message}")
class UserService:
def __init__(self, logger: Logger):
self.logger = logger
def register(self, username, password):
try:
# register user to database
print(f"User {username} registered successfully")
self.logger.log(f"User {username} registered successfully")
except Exception as e:
print(f"Error: {e}")
self.logger.log(f"Error: {e}")
logger = ConsoleLogger()
service = UserService(logger)
在這個例子中, UserService
現在只依賴 Logger
的抽象介面,而不是直接依賴實際的 Logger
物件。這個修改符合依賴反轉原則。
💡 何謂高層次模組、何謂低層次模組? 在軟體系統中,我們常會區分系統的不同層次,例如資料存取層、商業邏輯層、介面層等,資料存取層可能包含了一些和資料庫溝通的程式碼,而商業邏輯層則使用資料存取層中提供的方法來操作資料。在這種情況下,商業邏輯層可以被視為高層次模組,因為它使用了低層次模組的服務。