什麼是 SOLID 原則?

2023年3月7日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

什麼是 SOLID 原則?

SOLID 原則是一組軟體設計原則,用於指導軟體開發人員設計和實現高質量的、易於維護和擴展的軟體。它是由羅伯特·C·馬丁在其著作《Agile Software Development, Principles, Patterns, and Practices》中提出的,是目前軟體工程界被廣泛接受的一種軟體設計理念。

SOLID 五個原則

SOLID 原則包括以下五個原則:

  1. 單一職責原則(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()方法應該被拆分為一個獨立的類別或方法,以實現單一職責原則。

  1. 開放封閉原則(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 ****策略類別定義了一個共通的介面,可以用來擴展任何類型的折扣。TenPercentDiscountDiscount 的一個子類別,它實現了 10% 的折扣計算。ShoppingCartWithDiscount 類別持有一個 Discount 實例,並在計算總價時調用 calculate_discount() 方法。這樣,當我們需要新增一種不同類型的折扣時,只需要創建一個新的策略類別並將其傳遞給 ShoppingCartWithDiscount 即可,而無需修改現有的程式碼。

  1. 里氏替換原則(Liskov Substitution Principle,LSP)

所有引用基類別的地方必須能夠透明地使用其子類別的對象。換句話說,子類別應該可以替換其父類別並且不會破壞系統的正確性。

下面這個案例違反了 LSP,因為RectangleSquare 類別之間繼承關係是有問題的,因為 Square 繼承自 Rectangle,但是 Squareset_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)

這個範例中,RectangleSquare 都繼承自 Shape 類別,並且都實作了 area() 方法。在 print_area() 函數中,接受一個 Shape 物件作為引數,然後呼叫 area() 方法取得其面積。

這個範例中並沒有違反 LSP,因為 Square 物件可以成功地替換 Shape 物件,而不影響程式的正常執行。換句話說,Square 物件與 Shape 物件具有相同的行為,且能夠替代 Shape 物件,因此符合里氏替換原則。

  1. 介面隔離原則(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 是一個機器的介面,包含了 printfaxscan 三個方法。而 MultiFunctionPrinter 是一個具有多種功能的印表機,它繼承了 Machine 介面並實現了所有方法。這段程式碼違反了 ISP,因為不是所有的機器都需要實現 faxscan 方法,而 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 拆成了三個獨立的介面 PrinterFaxScannerMultiFunctionDevice 是一個具有多種功能的裝置,它實現了 PrinterFaxScanner 三個介面。這樣的設計讓每個介面只包含必要的方法,並讓裝置可以實現自己所需要的介面。這樣的設計更符合 ISP。

  1. 依賴反轉原則(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 物件。這個修改符合依賴反轉原則。

💡 何謂高層次模組、何謂低層次模組? 在軟體系統中,我們常會區分系統的不同層次,例如資料存取層、商業邏輯層、介面層等,資料存取層可能包含了一些和資料庫溝通的程式碼,而商業邏輯層則使用資料存取層中提供的方法來操作資料。在這種情況下,商業邏輯層可以被視為高層次模組,因為它使用了低層次模組的服務。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們