軟體開發中變更是不可避免的,如何應對變化是程式碼設計的關鍵。本文介紹「封裝變化」原則,強調將易變部分隔離,降低變更帶來的影響。透過支付系統範例,展示如何用多型封裝不同支付方式,新增支付方式時無需修改核心支付邏輯。此外,使用圓形類別範例,說明 Python 屬性如何封裝內部狀態,並透過 getter 和 setter 控制屬性存取,確保資料一致性。最後,文章提倡「組合優於繼承」原則,建議透過組合簡單物件構建複雜系統,提高程式碼的靈活性和可擴充套件性,避免繼承帶來的緊耦合問題。

前言

本文涵蓋的軟體/硬體環境 作業系統需求 Python 3.12 Windows、macOS 或 Linux MyPy 1.10.0 Docker Redis-server 6.2.6 LocalStack 3.4.0

如果您使用的是本文的電子版,建議您自行輸入程式碼或從本文的 GitHub 儲存庫存取程式碼(下一節將提供連結)。這樣做有助於避免因複製和貼上程式碼而導致的潛在錯誤。

下載範例程式碼檔案 您可以從 GitHub 下載本文的範例程式碼檔案:https://github.com/PacktPublishing/Mastering-Python-Design-Patterns-Third-Edition。如果程式碼有更新,將在 GitHub 儲存函式庫中更新。

我們還有其他程式碼包,來自我們豐富的書籍和影片目錄,您可以在 https://github.com/PacktPublishing/ 找到它們。請檢視!

使用慣例

本文中使用了一些文字慣例和格式規範。

大多數程式碼已經自動格式化 格式化的工作是使用 Black 工具完成的,這是 Python 開發人員出於生產力原因而常用的做法。因此,它可能看起來與您自己寫的程式碼不完全相同。但它是有效的;它是符合 PEP 8 的程式碼。目標是提高程式碼片段的可讀性。

因此,程式碼檔案中的一些程式碼片段以及本文頁面上的程式碼片段可能如下所示:

State = Enum(
    "State",
    "NEW RUNNING SLEEPING RESTART ZOMBIE",
)

另一個例子可能是:

msg = (
    f"trying to create process '{name}' "
    f"for user '{user}'"
)
print(msg)

本文頁面上的程式碼片段可能會被縮短 為了提高可讀性,當函式或類別有檔案字串(docstring)且過長時,我們會在書中的程式碼片段中將其刪除。

當某些程式碼(類別或函式)過長而無法在本章頁面上顯示時,我們可能會將其縮短,並指引讀者參考完整的程式碼檔案。

注意

如果遇到跨多行的長命令(使用「\」字元作為分隔符),您可以重新格式化長命令文字,刪除「\」字元,以確保命令在終端機中正確解釋。

其他慣例

文中程式碼:表示文中程式碼單詞、資料函式庫表格名稱、資料夾名稱、檔案名稱、檔案副檔名、路徑名稱、虛擬 URL、使用者輸入和 Twitter 帳號。以下是一個例子:「定義具有 log 方法的 Logger 介面。」

一段程式碼如下所示:

class MyInterface(ABC):
    @abstractmethod
    def do_something(self, param: str):
        pass

任何命令列輸入或輸出都寫成如下所示:

python3.12 –m pip install -–user mypy

粗體:表示新術語、重要單詞或您在螢幕上看到的單詞。例如,選單或對話方塊中的單詞以粗體顯示。以下是一個例子:「它是物件導向程式設計 OOP 中的核心概念之一,能夠讓單一介面代表不同型別。」

提示或重要注意事項

顯示如下。

與我們聯絡

我們始終歡迎讀者的回饋。

一般性回饋:如果您對本文的任何方面有疑問,請郵寄至 customercare@packtpub.com,並在郵件主題中提及書名。

勘誤:雖然我們已盡一切努力確保內容的準確性,但錯誤仍有可能發生。如果您在本文中發現錯誤,我們將非常感謝您向我們報告。請造訪 www.packtpub.com/support/errata 並填寫表格。

盜版:如果您在網際網路上發現任何形式的非法複製,請提供位置地址或網站名稱給我們。請聯絡 copyright@packtpub.com 並提供相關材料的連結。

如果您有興趣成為作者:如果您在某個領域具有專業知識,並有意撰寫或貢獻書籍,請造訪 authors.packtpub.com。

分享您的想法

閱讀完《精通 Python 設計模式》後,我們希望聽到您的想法!請點選此處直接前往本文的 Amazon 評論頁面並分享您的回饋。

您的評論對我們和技術社群非常重要,它將幫助我們確保提供卓越的內容品質。

下載免費 PDF 版本

感謝您購買本文!

您喜歡隨身閱讀,但無法攜帶印刷書籍嗎? 您的電子書購買內容與您選擇的裝置不相容嗎? 不用擔心,現在購買 Packt 書籍,您將免費獲得該書的無 DRM PDF 版本。

在任何地方、任何裝置上閱讀。直接從您喜愛的技術書籍中搜尋、複製和貼上程式碼到您的應用程式中。

福利還不止於此,您每天還可以透過電子郵件獲得獨家折扣、電子報和精彩的免費內容。

請依照以下簡單步驟獲得這些福利:

  1. 掃描 QR 碼或造訪以下連結:https://packt.link/free-ebook/9781837639618
  2. 提交您的購買證明
  3. 就這樣!我們將把您的免費 PDF 和其他福利直接傳送到您的電子信箱中

第一部分:從原則開始

第一部分將向您介紹基礎軟體設計原則和建立在其上的 S.O.L.I.D. 原則。本部分包括以下章節:

  • 第 1 章:基礎設計原則
  • 第 2 章:SOLID 原則

基礎設計原則:開發穩健軟體架構的根本

設計原則是任何良好架構軟體的基礎。它們如同指引開發者前進的燈塔,幫助開發者開發可維護、可擴充套件且穩健的應用程式,同時避開不良設計的陷阱。

在本章中,我們將探討所有開發者都應該瞭解並在專案中實踐的核心設計原則。我們將重點介紹四項基礎原則。首先,「封裝變化」原則教導我們如何隔離程式碼中易變的部分,使應用程式更易於修改和擴充套件。其次,「偏好組合」原則讓我們理解為何透過組合簡單物件來構建複雜物件往往優於繼承功能。第三,「針對介面程式設計」原則展示了針對介面而非具體類別進行程式設計的力量,從而增強了靈活性與可維護性。最後,「鬆散耦合」原則讓我們掌握減少元件間依賴性的重要性,使程式碼更易於重構和測試。

本章重點

  • 實踐「封裝變化」原則
  • 實踐「偏好組合而非繼承」原則
  • 實踐「針對介面程式設計,而非實作」原則
  • 實踐「鬆散耦合」原則

在本章結束時,您將對這些原則有深入的瞭解,並學會如何在 Python 中實踐它們,為本文的其餘部分奠定基礎。

技術需求

本文各章節需要 Python 3.12 環境(某些特殊章節可能需要 3.11)。此外,請透過以下指令安裝 Mypy 靜態型別檢查器:

python3.12 –m pip install -–user mypy

範例程式碼可在此 GitHub 儲存函式庫中找到:https://github.com/PacktPublishing/Mastering-Python-Design-Patterns-Third-Edition

Python 可執行檔說明

在本文中,我們將使用 python3.12python 來執行程式碼範例。請根據您的環境、實踐和工作流程進行調整。

實踐「封裝變化」原則

軟體開發中最常見的挑戰之一是應對變化。需求不斷演變,技術不斷進步,使用者需求也在變化。因此,撰寫能夠適應變化而不至於在整個程式或應用程式中造成連鎖反應的程式碼至關重要。這就是「封裝變化」原則發揮作用的地方。

意義

該原則背後的理念非常簡單:隔離程式碼中可能變化的部分並將其封裝起來。透過這種方式,您可以建立一道保護屏障,將程式碼的其他部分與這些易變的元素隔離開來。這種封裝使得您可以修改系統的一部分而不影響其他部分。

好處

封裝變化帶來多項好處,主要包括:

  • 易於維護:當需要進行變更時,您只需修改封裝的部分,減少了在其他地方引入錯誤的風險
  • 增強靈活性:封裝的元件可以輕鬆替換或擴充套件,提供更具適應性的架構
  • 提高可讀性:透過隔離易變的元素,您的程式碼變得更有條理、更容易理解

程式碼範例:封裝變化

from abc import ABC, abstractmethod

# 定義一個抽象類別作為介面
class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

# 實作不同的支付閘道
class PayPalGateway(PaymentGateway):
    def process_payment(self, amount):
        print(f"Processing PayPal payment of ${amount}")

class StripeGateway(PaymentGateway):
    def process_payment(self, amount):
        print(f"Processing Stripe payment of ${amount}")

# 使用封裝變化的支付處理器
class PaymentProcessor:
    def __init__(self, gateway: PaymentGateway):
        self.gateway = gateway

    def purchase(self, amount):
        self.gateway.process_payment(amount)

# 使用範例
if __name__ == "__main__":
    paypal_gateway = PayPalGateway()
    stripe_gateway = StripeGateway()

    paypal_processor = PaymentProcessor(paypal_gateway)
    stripe_processor = PaymentProcessor(stripe_gateway)

    paypal_processor.purchase(100)
    stripe_processor.purchase(200)

內容解密:

  1. 抽象類別與介面定義:我們使用 ABC@abstractmethod 定義了一個抽象類別 PaymentGateway,其中包含一個抽象方法 process_payment。這為不同的支付閘道提供了一個統一的介面。
  2. 具體支付閘道實作PayPalGatewayStripeGatewayPaymentGateway 的具體實作,分別代表不同的支付閘道。它們各自實作了 process_payment 方法,以處理特定的支付邏輯。
  3. 支付處理器類別PaymentProcessor 類別負責處理支付請求。它接受一個 PaymentGateway 物件作為引數,並在 purchase 方法中呼叫該閘道的 process_payment 方法。這樣,支付處理邏輯與具體的支付閘道實作解耦。
  4. 使用範例:在 if __name__ == "__main__": 部分,我們建立了 PayPalGatewayStripeGateway 的例項,並將它們傳遞給 PaymentProcessor。這樣,我們可以根據需要選擇不同的支付閘道來處理支付請求。

這個範例展示瞭如何透過封裝變化來提高程式碼的靈活性和可維護性。當需要新增或替換支付閘道時,我們只需建立新的 PaymentGateway 實作,而無需修改現有的 PaymentProcessor 邏輯。

實作封裝的技術

正如我們所介紹的,封裝有助於資料隱藏和只暴露必要的功能。在這裡,我們將介紹增強 Python 中封裝性的關鍵技術:多型和 getter、setter 技術。

多型

在程式設計中,多型允許不同類別的物件被視為共同超類別的物件。它是物件導向程式設計(OOP)中的核心概念之一,能夠使單一介面代表不同型別。多型允許實作優雅的軟體設計模式,例如策略模式,並且是實作 Python 中乾淨、可維護程式碼的一種方式。

Getter 和 Setter

這些是類別中的特殊方法,能夠控制對屬性值的存取。getter 允許讀取屬性值,而 setter 允許修改它們。透過使用這些方法,您可以新增驗證邏輯或日誌記錄等副作用,從而遵循封裝原則。它們提供了一種控制和保護物件狀態的方式,在您想要封裝由其他例項變數衍生的複雜屬性時特別有用。

此外,Python 提供了一種更優雅的方法,稱為屬性(property)技術,以補充 getter 和 setter 技術。這是 Python 的內建功能,允許您將屬性存取轉換為方法呼叫。透過屬性,您可以確保物件保持其內部狀態,避免錯誤或有害的操作,而無需明確定義 getter 和 setter 方法。

@property 裝飾器允許您定義一個在存取屬性時自動呼叫的方法,有效地充當 getter。同樣,@attribute_name.setter 裝飾器允許您定義一個充當 setter 的方法,在嘗試更改屬性值時呼叫。這樣,您可以直接在這些方法中嵌入驗證或其他操作,使程式碼更加乾淨。

透過使用屬性技術,您可以實作與傳統 Getter 和 Setter 相同程度的資料封裝和驗證,但方式更符合 Python 的設計哲學。它允許您撰寫不僅功能強大,而且乾淨易讀的程式碼,從而提高封裝性和 Python 程式的整體品質。

接下來,我們將透過範例更好地理解這些技術。

封裝變化的設計原則實作範例

在軟體設計中,封裝變化是一個重要的設計原則,旨在將系統中可能變化的部分隔離出來,以提高程式碼的可維護性和擴充套件性。以下將透過兩個範例來說明如何使用多型和屬性(property)來實作封裝變化。

使用多型進行封裝

多型是一種強大的技術,可以實作對不同行為的封裝。以下以一個支付處理系統為例,展示如何使用多型來封裝不同的支付方式。

設計與實作

  1. 定義基底類別:首先定義一個名為 PaymentBase 的基底類別,其中包含一個 process_payment 方法,供具體的支付方式實作。

    class PaymentBase:
        def __init__(self, amount: int):
            self.amount: int = amount
    
        def process_payment(self):
            pass
    
  2. 實作具體支付方式:接著,建立 CreditCardPayPal 類別,分別繼承自 PaymentBase 並實作 process_payment 方法。這種實作方式是多型的典型應用,可以將 CreditCardPayPal 物件視為其共同父類別的例項。

    class CreditCard(PaymentBase):
        def process_payment(self):
            msg = f"Credit card payment: {self.amount}"
            print(msg)
    
    class PayPal(PaymentBase):
        def process_payment(self):
            msg = f"PayPal payment: {self.amount}"
            print(msg)
    
  3. 測試類別:為了測試剛建立的類別,可以新增一些程式碼,呼叫每個物件的 process_payment 方法。多型的優勢在於使用這些類別時體現得淋漓盡致,如下所示:

    if __name__ == "__main__":
        payments = [CreditCard(100), PayPal(200)]
        for payment in payments:
            payment.process_payment()
    

完整的程式碼如下:

class PaymentBase:
    def __init__(self, amount: int):
        self.amount: int = amount
    
    def process_payment(self):
        pass

class CreditCard(PaymentBase):
    def process_payment(self):
        msg = f"Credit card payment: {self.amount}"
        print(msg)

class PayPal(PaymentBase):
    def process_payment(self):
        msg = f"PayPal payment: {self.amount}"
        print(msg)

if __name__ == "__main__":
    payments = [CreditCard(100), PayPal(200)]
    for payment in payments:
        payment.process_payment()

執行此程式碼後,將輸出:

Credit card payment: 100
PayPal payment: 200

#### 內容解密:

  • 此範例展示瞭如何使用多型來封裝不同的支付方式。
  • PaymentBase 類別定義了介面,而 CreditCardPayPal 類別提供了具體的實作。
  • 這種設計使得新增新的支付方式或修改現有的支付方式變得容易,而不會影響核心的支付處理邏輯。

使用屬性(property)進行封裝

接下來,定義一個 Circle 類別,並展示如何使用 Python 的 @property 技術來建立其半徑屬性的 getter 和 setter。

設計與實作

  1. 定義 Circle 類別:首先定義 Circle 類別及其初始化方法,其中初始化 _radius 屬性。

    class Circle:
        def __init__(self, radius: int):
            self._radius: int = radius
    
  2. 新增 radius 屬性:使用 @property 修飾符定義一個 radius 方法,該方法傳回底層屬性的值。

    @property
    def radius(self):
        return self._radius
    
  3. 新增 radius setter:定義另一個 radius 方法,用於修改底層屬性,並進行驗證檢查,以避免半徑為負值。此方法使用 @radius.setter 修飾符。

    @radius.setter
    def radius(self, value: int):
        if value < 0:
            raise ValueError("Radius cannot be negative!")
        self._radius = value
    
  4. 測試類別:新增一些程式碼來測試 Circle 類別。

    if __name__ == "__main__":
        circle = Circle(10)
        print(f"Initial radius: {circle.radius}")
        circle.radius = 15
        print(f"New radius: {circle.radius}")
    

完整的程式碼如下:

class Circle:
    def __init__(self, radius: int):
        self._radius: int = radius
    
    @property
    def radius(self):
        return self._radius
    
    @radius.setter
    def radius(self, value: int):
        if value < 0:
            raise ValueError("Radius cannot be negative!")
        self._radius = value

if __name__ == "__main__":
    circle = Circle(10)
    print(f"Initial radius: {circle.radius}")
    circle.radius = 15
    print(f"New radius: {circle.radius}")

執行此程式碼後,將輸出:

Initial radius: 10
New radius: 15

#### 內容解密:

  • 此範例展示瞭如何使用 @property@radius.setter 來封裝 Circle 類別的半徑屬性。
  • 這種設計使得我們可以在不破壞類別介面的情況下,改變技術實作或新增驗證邏輯。

優先使用組合而非繼承

在物件導向程式設計(OOP)中,建立複雜的類別層次結構很誘人,但這可能導致緊耦合的程式碼,難以維護和擴充套件。因此,建議優先使用組合而非繼承,即透過組合簡單的物件來建立複雜的物件。

這種設計原則鼓勵開發者透過組合簡單、獨立的元件來建立複雜系統,而不是透過繼承機制。這樣做的好處包括提高程式碼的可維護性、降低耦合度以及增強系統的可擴充套件性。

#### 圖表翻譯:

此圖示展示了使用組合而非繼承的設計思路,可以用 Mermaid 圖表呈現不同元件之間的關係和互動。

  graph LR;
    A[複雜系統] --> B[簡單元件1];
    A --> C[簡單元件2];
    A --> D[簡單元件3];

圖表翻譯:

  • 此圖表示展示瞭如何透過組合多個簡單元件來構建一個複雜系統。
  • 每個簡單元件負責特定的功能或行為,從而降低了整個系統的複雜度並提高了可維護性。