SOLID 設計原則是軟體工程中重要的設計理念,旨在提高程式碼品質和可維護性。本文以 Python 語言為例,講解如何應用開放封閉原則(OCP)、里氏替換原則(LSP)、介面隔離原則(ISP)和依賴反轉原則(DIP),並搭配程式碼範例說明如何透過 Protocol 和介面抽象化提升程式碼的彈性。此外,文章也概述了建立式設計模式,例如工廠模式和建構者模式,為讀者提供更廣泛的軟體設計思維。這些原則和模式能有效降低程式碼耦合度,提升程式碼的可測試性和可重用性,是軟體工程師必備的知識。

SOLID 設計原則實務應用

在軟體開發領域中,SOLID 設計原則是一套被廣泛接受且重要的指導方針,幫助開發者設計出更具彈性、可維護性及可擴充套件性的系統。本篇將探討 SOLID 中的開放封閉原則(Open-Closed Principle, OCP)及里氏替換原則(Liskov Substitution Principle, LSP),並透過實際的 Python 程式碼範例來說明如何應用這些原則。

開放封閉原則(OCP)實踐

開放封閉原則強調軟體實體(類別、模組、函式等)應該對擴充套件開放,但對修改封閉。也就是說,在不修改現有程式碼的情況下,能夠新增新的功能。以下是一個實際的範例:

不符合 OCP 的設計

假設我們有一個 calculate_area 函式,用於計算不同形狀的面積:

def calculate_area(shape):
    if shape.type == 'rectangle':
        return shape.width * shape.height
    elif shape.type == 'circle':
        return math.pi * (shape.radius ** 2)

這種設計明顯違反了 OCP,因為每當我們需要新增新的形狀時,都需要修改 calculate_area 函式。

符合 OCP 的設計

透過使用 Protocol 和多型,我們可以改進設計,使其符合 OCP:

import math
from typing import Protocol

class Shape(Protocol):
    def area(self) -> float:
        ...

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width: float = width
        self.height: float = height

    def area(self) -> float:
        return self.width * self.height

class Circle:
    def __init__(self, radius: float):
        self.radius: float = radius

    def area(self) -> float:
        return math.pi * (self.radius ** 2)

def calculate_area(shape: Shape) -> float:
    return shape.area()

if __name__ == "__main__":
    rect = Rectangle(12, 8)
    rect_area = calculate_area(rect)
    print(f"Rectangle area: {rect_area}")

    circ = Circle(6.5)
    circ_area = calculate_area(circ)
    print(f"Circle area: {circ_area:.2f}")

內容解密:

  1. 使用 Protocol 定義介面Shape Protocol 定義了 area 方法,所有實作此 Protocol 的類別都必須提供 area 方法的實作。
  2. 多型應用calculate_area 函式接受任何符合 Shape Protocol 的物件,並呼叫其 area 方法,無需關心具體的形狀型別。
  3. 擴充套件性:新增形狀型別(如 Triangle)時,只需建立新的類別並實作 Shape Protocol,無需修改 calculate_area 函式。

里氏替換原則(LSP)實踐

里氏替換原則指出,任何使用父類別物件的地方,都能夠使用子類別物件替代,而不會影響程式的正確性。以下是一個違反 LSP 的例子及其改進方案。

違反 LSP 的設計

class Bird:
    def fly(self):
        print("I can fly")

class Penguin(Bird):
    def fly(self):
        print("I can't fly")

def make_bird_fly(bird):
    bird.fly()

在這個例子中,PenguinBird 的子類別,但它無法飛行,這違反了 LSP,因為替換 Bird 物件為 Penguin 物件會改變程式的行為。

符合 LSP 的設計

class Bird:
    def move(self):
        print("I'm moving")

class FlyingBird(Bird):
    def move(self):
        print("I'm flying")

class FlightlessBird(Bird):
    def move(self):
        print("I'm walking")

def make_bird_move(bird):
    bird.move()

if __name__ == "__main__":
    generic_bird = Bird()
    eagle = FlyingBird()
    penguin = FlightlessBird()

    make_bird_move(generic_bird)
    make_bird_move(eagle)
    make_bird_move(penguin)

內容解密:

  1. 重新定義父類別行為:將 Bird 類別的 fly 方法改為更一般的 move 方法,以涵蓋不同鳥類別的移動方式。
  2. 建立更具體的子類別:引入 FlyingBirdFlightlessBird 兩個子類別,分別代表會飛和不會飛的鳥,它們都繼承自 Bird 並根據自身特性實作 move 方法。
  3. 保持行為的一致性:透過這種設計,無論是 BirdFlyingBird 還是 FlightlessBird,都可以被 make_bird_move 函式正確處理,確保了替換的正確性。

遵循 SOLID 設計原則能夠顯著提升軟體的品質和可維護性。本文透過 Python 範例展示瞭如何應用 OCP 和 LSP 來設計更具彈性和穩定性的軟體系統。希望這些實踐經驗能對您的軟體開發工作有所幫助。

介面隔離原則(ISP)與依賴反轉原則(DIP)實務解析

在軟體設計中,遵循SOLID原則能夠顯著提升程式碼的可維護性與擴充套件性。本文將探討介面隔離原則(ISP)與依賴反轉原則(DIP),並透過具體範例展示如何在Python中實踐這些設計原則。

介面隔離原則(ISP)實務應用

ISP主張設計更小、更具體的介面,而非廣泛的通用介面。這意味著類別不應被迫實作它們不需要的介面。在Python中,這可以透過Protocol來定義介面。

傳統設計的問題

假設我們有一個AllInOnePrinter類別,實作了列印、掃描和傳真功能。如果我們要新增一個只具備列印功能的SimplePrinter類別,按照傳統設計,它可能需要繼承或實作所有這些功能,即使它只關心列印。

class AllInOnePrinter:
    def print_document(self):
        print("Printing")

    def scan_document(self):
        print("Scanning")

    def fax_document(self):
        print("Faxing")

遵循ISP的最佳實踐

為瞭解決上述問題,我們可以為每個功能定義獨立的介面,讓每個類別只實作它需要的介面。

  1. 定義介面:使用Protocol定義PrinterScannerFax介面。

    from typing import Protocol
    
    class Printer(Protocol):
        def print_document(self) -> None:
            ...
    
    class Scanner(Protocol):
        def scan_document(self) -> None:
            ...
    
    class Fax(Protocol):
        def fax_document(self) -> None:
            ...
    
  2. 實作類別AllInOnePrinter實作所有介面,而SimplePrinter只實作Printer介面。

    class AllInOnePrinter:
        def print_document(self) -> None:
            print("Printing")
    
        def scan_document(self) -> None:
            print("Scanning")
    
        def fax_document(self) -> None:
            print("Faxing")
    
    class SimplePrinter:
        def print_document(self) -> None:
            print("Simply Printing")
    
  3. 使用介面:定義一個函式,接受任何實作了Printer介面的物件。

    def do_the_print(printer: Printer) -> None:
        printer.print_document()
    
  4. 測試

    if __name__ == "__main__":
        all_in_one = AllInOnePrinter()
        all_in_one.scan_document()
        all_in_one.fax_document()
        do_the_print(all_in_one)
    
        simple = SimplePrinter()
        do_the_print(simple)
    

執行結果:

Scanning
Faxing
Printing
Simply Printing

#### 內容解密:

此範例展示瞭如何透過ISP改進設計。透過定義更小的介面,我們使SimplePrinter類別不需要實作它不需要的功能,從而提高了程式碼的模組化和可維護性。

依賴反轉原則(DIP)實務應用

DIP主張高層模組不應直接依賴低層模組,而應都依賴於抽象或介面。透過這種方式,可以減少系統各部分之間的耦合度。

傳統設計的問題

假設有一個Notification類別負責透過電子郵件傳送通知,使用了一個Email類別。原始設計中,Notification直接依賴於Email

class Email:
    def send_email(self, message: str) -> None:
        print(f"Sending email: {message}")

class Notification:
    def __init__(self) -> None:
        self.email = Email()

    def send(self, message: str) -> None:
        self.email.send_email(message)

遵循DIP的最佳實踐

  1. 定義抽象:引入一個MessageSender介面。

    class MessageSender(Protocol):
        def send(self, message: str) -> None:
            ...
    
  2. 修改Email類別:讓Email類別實作MessageSender介面。

    class Email:
        def send(self, message: str) -> None:
            print(f"Sending email: {message}")
    
  3. 修改Notification類別:讓它依賴於MessageSender介面,而不是具體的Email類別。

    class Notification:
        def __init__(self, sender: MessageSender) -> None:
            self.sender = sender
    
        def send(self, message: str) -> None:
            self.sender.send(message)
    

#### 內容解密:

透過引入MessageSender介面,我們解耦了高層的Notification類別和低層的Email類別。這使得系統更容易擴充套件和維護,例如,可以輕鬆替換為其他訊息傳送方式。

SOLID 設計原則與建立式設計模式解析

在軟體開發領域中,SOLID 原則是提升程式碼可維護性、擴充套件性和可讀性的重要指導方針。本文將探討依賴反轉原則(Dependency Inversion Principle, DIP)及其在 Python 中的應用,並進一步介紹建立式設計模式(Creational Design Patterns)的基本概念與實踐方法。

依賴反轉原則(DIP)實踐

依賴反轉原則主張高層模組不應依賴於低層模組,而應共同依賴於抽象介面。以下是一個根據 Python 的範例,用於展示如何透過 DIP 重構程式碼。

原始程式碼重構

假設我們需要實作一個通知系統,能夠透過電子郵件或其他方式傳送訊息。首先,我們定義一個 MessageSender 協定(Protocol),它包含一個 send 方法。

from typing import Protocol

class MessageSender(Protocol):
    def send(self, message: str):
        ...

接著,我們建立一個 Email 類別來實作 MessageSender 介面。

class Email:
    def send(self, message: str):
        print(f"Sending email: {message}")

然後,我們定義一個 Notification 類別,它依賴於 MessageSender 介面來傳送訊息。

class Notification:
    def __init__(self, sender: MessageSender):
        self.sender = sender
    
    def send(self, message: str):
        self.sender.send(message)

最後,我們測試這個設計。

if __name__ == "__main__":
    email = Email()
    notif = Notification(sender=email)
    notif.send(message="This is the message.")

程式碼解密

  1. MessageSender 協定定義:透過定義 MessageSender 協定,我們建立了一個抽象層,使得 Notification 類別可以依賴於這個抽象層而非具體的實作類別(如 Email)。
  2. Email 類別實作Email 類別實作了 MessageSender 協定,提供了具體的 send 方法實作。
  3. Notification 類別設計Notification 類別透過依賴於 MessageSender 協定,實作了與具體訊息傳送方式的解耦。
  4. 測試:在測試程式碼中,我們例項化了 EmailNotification 物件,並成功發送了一條訊息。

建立式設計模式概覽

建立式設計模式關注於物件的建立機制,旨在提供更靈活、更可維護的物件建立方式。本章節將介紹多種建立式設計模式,包括工廠模式(Factory Pattern)、建構者模式(Builder Pattern)、原型模式(Prototype Pattern)、單例模式(Singleton Pattern)以及物件池模式(Object Pool Pattern)。

工廠模式

工廠模式是一種常見的建立式設計模式,它提供了一種封裝物件建立邏輯的方式,使得客戶端程式碼不需要直接例項化具體類別。工廠模式主要分為工廠方法和抽象工廠兩種型別。

工廠方法範例

考慮一個塑膠玩具製造的例子,不同的玩具可以透過相同的模具材料和不同的模具形狀來生產。這類別似於工廠方法模式,其中輸入是所需的玩具型別,輸出是對應的塑膠玩具。

在軟體開發中,Django 網頁框架利用工廠方法模式來建立網頁表單的不同欄位型別,如 CharFieldEmailField