單例模式和物件池模式都是常見的軟體設計模式,用於管理物件的建立和使用。單例模式確保一個類別只有一個例項,並提供全域性存取點。這在需要分享資源或協調系統操作時非常有用,例如資料函式庫連線或日誌記錄器。物件池模式則預先建立一組物件,並允許重複使用,從而減少頻繁建立和銷毀物件的開銷,適用於初始化成本較高的物件,例如資料函式庫連線或執行緒。本文將以 Python 程式碼示範如何實作這兩種模式,並探討它們的應用場景和優缺點,同時也將介紹介面卡模式,說明如何橋接不相容的介面。

在 Python 中,可以使用元類別優雅地實作單例模式,透過覆寫 __call__ 方法來控制類別的例項化過程,確保只建立一個例項。物件池模式則可以透過維護一個可用物件列表和一個使用中物件列表來實作,並提供取得和釋放物件的方法。介面卡模式則可以透過定義一個新的類別,將一個類別的介面轉換成另一個類別的介面,從而讓不相容的類別能夠一起工作。理解並運用這些設計模式,能有效提升程式碼的可維護性、可重用性和效能。

單例模式(Singleton Pattern)深度解析

單例模式是物件導向程式設計(OOP)中最原始的設計模式之一,其主要目的是限制某個類別的例項化次數,使其只能建立一個物件。這種模式在需要一個物件來協調系統中的各種操作時非常有用。

真實世界中的範例

在現實生活中,船長或船隻的指揮官就是單例模式的典型例子。在船上,他們是唯一的指揮者,負責做出重要的決定,並處理各種請求。

另一個例子是辦公室環境中的印表機後台處理程式,它確保列印任務透過單一節點進行協調,避免衝突並確保有序列印。

單例模式的應用場景

單例設計模式在需要建立一個物件或需要某種能夠維護程式全域狀態的物件時非常有用。其他可能的應用場景包括:

  • 控制對分享資源的平行存取,例如管理資料函式庫連線的類別。
  • 提供一個橫向的服務或資源,可以從應用程式的不同部分或不同使用者存取,例如日誌系統或公用程式的核心類別。

實作單例模式

單例模式確保某個類別只有一個例項,並提供一個全域存取點來存取它。在這個例子中,我們將建立一個 URLFetcher 類別,用於從網頁中擷取內容。我們希望確保這個類別只有一個例項,以便追蹤所有已擷取的 URL。

首先,我們建立一個簡單版本的 URLFetcher 類別。這個類別有一個 fetch() 方法,用於擷取網頁內容並將 URL 儲存在列表中:

import urllib.request

class URLFetcher:
    def __init__(self):
        self.urls = []

    def fetch(self, url):
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            if response.code == 200:
                page_content = response.read()
                with open("content.html", "a") as f:
                    f.write(str(page_content))
                self.urls.append(url)

內容解密:

  1. __init__ 方法初始化 urls 列表,用於儲存已擷取的 URL。
  2. fetch 方法使用 urllib.request 模組擷取指定 URL 的內容。
  3. 如果 HTTP 請求成功(狀態碼 200),則將網頁內容寫入 content.html 檔案,並將 URL 新增至 urls 列表。

為了檢查我們的類別是否為單例,可以使用 is 運算元比較兩個類別例項。如果它們相同,則表示是單例:

if __name__ == "__main__":
    print(URLFetcher() is URLFetcher())

執行此程式碼後,會發現輸出為 False,表示目前版本的類別尚未實作單例模式。

使用元類別(Metaclass)實作單例模式

在 Python 中,元類別是一種定義類別行為的類別。我們將建立一個 SingletonType 元類別,以確保 URLFetcher 類別只有一個例項:

class SingletonType(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            obj = super(SingletonType, cls).__call__(*args, **kwargs)
            cls._instances[cls] = obj
        return cls._instances[cls]

現在,我們修改 URLFetcher 類別以使用此元類別:

class URLFetcher(metaclass=SingletonType):
    def __init__(self):
        self.urls = []

    def fetch(self, url):
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            if response.code == 200:
                page_content = response.read()
                with open("content.html", "a") as f:
                    f.write(str(page_content))
                self.urls.append(url)

內容解密:

  1. SingletonType 元類別覆寫了 __call__ 方法,以控制 URLFetcher 類別的例項化。
  2. 如果 URLFetcher 類別尚未被例項化,則建立一個新例項並儲存在 _instances 字典中。
  3. URLFetcher 的後續呼叫將傳回相同的例項。

最後,我們建立一個 main() 函式並呼叫它來測試我們的單例:

def main():
    my_urls = [
        "http://python.org",
        "https://planetpython.org/",
        "https://www.djangoproject.com/",
    ]
    print(URLFetcher() is URLFetcher())
    fetcher = URLFetcher()
    for url in my_urls:
        fetcher.fetch(url)
    print(f"Done URLs: {fetcher.urls}")

if __name__ == "__main__":
    main()

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

True
Done URLs: ['http://python.org', 'https://planetpython.org/', 'https://www.djangoproject.com/']

此外,還會發現建立了一個名為 content.html 的檔案,其中包含從不同 URL 擷取的 HTML 文字。

這證明瞭單例模式的有效性,以及如何在 Python 中使用元類別實作它。

物件池模式的實務應用

物件池模式是一種建立設計模式,它允許重複使用現有的物件,而不是在每次需要時都建立新的物件。當初始化新物件的成本(以系統資源、時間等衡量)較高時,這種模式尤其有用。

現實世界的例子

考慮一個汽車租賃服務。當客戶租用汽車時,服務並不會為他們製造一輛新車。相反,它從可用的汽車池中提供一輛。當客戶歸還汽車時,它會回到池中,準備供下一個客戶使用。

另一個例子是公共遊泳池。與其每次有人想游泳時都重新注滿水,池中的水會被處理和重複使用,以節省時間和資源。

物件池模式的使用場景

物件池模式在資源初始化成本高或耗時長的場景中尤其有用。這可能涉及CPU週期、記憶體使用或甚至網路頻寬。例如,在射擊遊戲中,您可能會使用此模式來管理子彈物件。每次開火都建立一個新的子彈可能是資源密集型的。相反,您可以擁有一個可重複使用的子彈物件池。

實作物件池模式

讓我們實作一個可重複使用的汽車物件池,用於汽車租賃應用程式,以避免重複建立和銷毀它們。

首先,我們需要定義一個Car類別,如下所示:

class Car:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model
        self.in_use = False

物件池實作詳解

接下來,我們開始定義一個CarPool類別及其初始化,如下所示:

class CarPool:
    def __init__(self):
        self._available = []
        self._in_use = []
acquire_car 方法實作
def acquire_car(self) -> Car:
    if len(self._available) == 0:
        new_car = Car("BMW", "M3")
        self._available.append(new_car)
    car = self._available.pop()
    self._in_use.append(car)
    car.in_use = True
    return car

內容解密:

  1. 檢查是否有可用的汽車。如果沒有,則建立一輛新車並新增到可用汽車列表中。
  2. 從可用列表中取出汽車,並將其新增到使用中的汽車列表。
  3. 將汽車的in_use屬性設為True,表示該車正在被使用。
release_car 方法實作
def release_car(self, car: Car) -> None:
    car.in_use = False
    self._in_use.remove(car)
    self._available.append(car)

內容解密:

  1. 將汽車的in_use屬性設為False,表示該車不再被使用。
  2. 從使用中的汽車列表中移除該車。
  3. 將該車增加回可用汽車列表中。

測試實作結果

最後,我們新增一些程式碼來測試實作結果,如下所示:

if __name__ == "__main__":
    pool = CarPool()
    car_name = "Car 1"
    print(f"Acquire {car_name}")
    car1 = pool.acquire_car()
    print(f"{car_name} in use: {car1.in_use}")
    print(f"Now release {car_name}")
    pool.release_car(car1)
    print(f"{car_name} in use: {car1.in_use}")

執行結果如下:

Acquire Car 1
Car 1 in use: True
Now release Car 1
Car 1 in use: False

這表明我們的物件池模式實作按預期工作。

結構性設計模式

在前一章中,我們討論了建立型模式和物件導向程式設計模式,這些模式幫助我們處理物件建立的過程。下一類別的模式是結構性設計模式。結構性設計模式提出了一種組合物件以提供新功能的方法。

在本章中,我們將涵蓋以下主要主題:

  • 介面卡模式(The Adapter Pattern)
  • 裝飾模式(The Decorator Pattern)
  • 橋接模式(The Bridge Pattern)
  • 外觀模式(The Facade Pattern)
  • 享元模式(The Flyweight Pattern)
  • 代理模式(The Proxy Pattern)

在本章結束時,您將具備使用結構性設計模式有效地組織程式碼的技能。

技術需求

請參閱第一章中提出的技術需求。

介面卡模式

介面卡模式是一種結構性設計模式,幫助我們使兩個不相容的介面相容。這到底是什麼意思?如果我們有一個舊元件,並希望在新系統中使用它,或者有一個新元件,並希望在舊系統中使用它,那麼這兩個元件很少能在不需要任何程式碼更改的情況下進行通訊。但是更改程式碼並不總是可行的,因為我們可能無法存取原始碼,或者因為這樣做是不切實際的。在這種情況下,我們可以編寫一個額外的層,使所有必需的修改能夠在兩個介面之間進行通訊。這個層被稱為介面卡。

真實世界的例子

當您從大多數歐洲國家旅行到英國或美國,或反之亦然時,您需要使用插頭介面卡為您的筆記型電腦充電。同樣的介面卡也需要將某些裝置連線到您的電腦:USB 介面卡。

在軟體類別中,zope.interface 套件(https://pypi.org/project/zope.interface/)是 Zope Toolkit(ZTK)的一部分,提供了定義介面和執行介面適配的工具。這些工具被用於多個 Python 網頁框架專案的核心(包括 Pyramid 和 Plone)。

使用案例

通常,兩個不相容的介面之一是外部或舊/遺留的。如果介面是外部的,則意味著我們無法存取原始碼。如果是舊的,則通常不切實際地對其進行重構。使用介面卡使事情在實作後能夠運作是一種好的方法,因為它不需要存取外部介面的原始碼。如果我們必須重複使用一些遺留程式碼,它也是一種實用的解決方案。然而,請注意,它可能會引入難以除錯的副作用。因此,請謹慎使用。

實作介面卡模式 - 適配遺留類別

讓我們考慮一個例子,我們有一個遺留支付系統和一個新的支付閘道。介面卡模式可以使它們在不更改現有程式碼的情況下協同工作,正如我們將要看到的那樣。

遺留支付系統使用一個類別來實作,具有一個 make_payment() 方法來執行支付工作的核心,如下所示:

class OldPaymentSystem:
    def __init__(self, currency):
        self.currency = currency

    def make_payment(self, amount):
        print(f"[OLD] Pay {amount} {self.currency}")

新的支付系統使用以下類別來實作,提供了一個 execute_payment() 方法:

class NewPaymentGateway:
    def __init__(self, currency):
        self.currency = currency

    def execute_payment(self, amount):
        print(f"Execute payment of {amount} {self.currency}")

現在,我們將新增一個類別來提供適配。我們的介面卡類別具有一個屬性 system 來儲存代表我們需要適配的支付系統的物件,我們稱之為適配者。它還具有一個 make_payment() 方法,在其中呼叫 execute_payment() 方法在適配者物件上以完成支付。程式碼如下:

class PaymentAdapter:
    def __init__(self, system):
        self.system = system

    def make_payment(self, amount):
        self.system.execute_payment(amount)

程式碼解密:

  1. PaymentAdapter 類別用於適配 NewPaymentGateway 的介面以符合 OldPaymentSystem 的介面。
  2. make_payment() 方法中,我們呼叫了 execute_payment() 方法在 self.system 物件上,這是 NewPaymentGateway 的例項。
  3. 這種適配技術使我們能夠使用新的支付閘道與現有程式碼協同工作,而無需更改現有程式碼。

讓我們透過新增一個 main() 函式和測試程式碼來檢視此適配的結果,如下所示:

def main():
    old_system = OldPaymentSystem("euro")
    print(old_system)
    new_system = NewPaymentGateway("euro")
    print(new_system)
    adapter = PaymentAdapter(new_system)
    adapter.make_payment(100)

if __name__ == "__main__":
    main()

執行程式碼應該會輸出以下內容:

<__main__.OldPaymentSystem object at 0x10ee58fd0>
<__main__.NewPaymentGateway object at 0x10ee58f70>
Execute payment of 100 euro

現在,您瞭解了這種適配技術如何使我們能夠使用新的支付閘道與現有程式碼協同工作,而無需更改現有程式碼。

實作介面卡模式 - 將多個類別適配到統一介面

讓我們看看另一個應用程式來說明適配:俱樂部的活動。我們的俱樂部有兩個主要活動:

  • 聘請有才華的藝術家在俱樂部表演
  • 組織表演和活動以娛樂客戶

在核心部分,我們有一個 Club 類別,代表藝術家表演的俱樂部。organize_event() 方法是俱樂部可以執行的主要操作。程式碼如下:

class Club:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"the club {self.name}"

    def organize_event(self):
        return "hires an artist to perform"

大多數時候,我們的俱樂部會聘請 DJ 來表演,但我們的應用程式應該允許組織多樣化的表演:由音樂家或樂隊、舞者、一人或一人表演等。