在軟體開發過程中,隨著系統規模的擴大,程式碼的複雜度也隨之提升。設計模式提供了一套經過驗證的解決方案,可以有效地應對這些挑戰。本文將探討四種結構型設計模式:介面卡模式、裝飾器模式、橋接模式和門面模式,並結合實際案例,分析它們如何提升程式碼的可維護性、可擴充套件性和靈活性。介面卡模式著重於解決介面不相容的問題,允許不同介面的物件協同工作。裝飾器模式則專注於動態地為物件新增功能,而無需修改其原始結構。橋接模式將抽象部分與實作部分分離,使兩者可以獨立變化。最後,門面模式提供了一個簡化的介面,隱藏了底層系統的複雜性。
結構性設計模式:介面卡模式與裝飾器模式
在軟體開發中,重用現有程式碼是提高開發效率和降低成本的重要手段。然而,不同程式碼之間的介面不相容問題常常出現。本文將介紹兩種結構性設計模式:介面卡模式和裝飾器模式,並探討如何使用這兩種模式解決介面不相容問題和動態擴充套件物件功能。
介面卡模式
介面卡模式是一種結構性設計模式,允許將不同介面的物件轉換為統一的介面,以便客戶端程式碼可以無縫地使用這些物件。
實際案例
假設我們有一個外部函式庫,提供了 Musician 和 Dancer 兩個類別,分別具有 play() 和 dance() 方法。然而,我們的客戶端程式碼只知道如何呼叫 organize_performance() 方法(在 Club 類別上),而不知道 play() 或 dance() 方法。
# external.py
class Musician:
def __init__(self, name):
self.name = name
def __str__(self):
return f"the musician {self.name}"
def play(self):
return "plays music"
class Dancer:
def __init__(self, name):
self.name = name
def __str__(self):
return f"the dancer {self.name}"
def dance(self):
return "does a dance performance"
介面卡實作
為瞭解決這個問題,我們可以建立一個通用的 Adapter 類別,將不同介面的物件轉換為統一的介面。
# adapt_to_unified_interface.py
from external import Musician, Dancer
class Club:
def __init__(self, name):
self.name = name
def __str__(self):
return f"the club {self.name}"
def organize_performance(self):
return "hires an artist to perform"
class Adapter:
def __init__(self, obj, adapted_methods):
self.obj = obj
self.__dict__.update(adapted_methods)
def __str__(self):
return str(self.obj)
def main():
objects = [
Club("Jazz Cafe"),
Musician("Roy Ayers"),
Dancer("Shane Sparks"),
]
for obj in objects:
if hasattr(obj, "play") or hasattr(obj, "dance"):
if hasattr(obj, "play"):
adapted_methods = dict(organize_event=obj.play)
elif hasattr(obj, "dance"):
adapted_methods = dict(organize_event=obj.dance)
obj = Adapter(obj, adapted_methods)
print(f"{obj} {obj.organize_event()}" if hasattr(obj, 'organize_event') else f"{obj} {obj.organize_performance()}")
if __name__ == "__main__":
main()
內容解密:
- Adapter類別的作用:
Adapter類別允許我們將不同介面的物件轉換為統一的介面,使得客戶端程式碼可以無縫地使用這些物件。 __init__方法:初始化物件並更新其屬性字典,以包含適配後的方法。__str__方法:傳回被適配物件的字串表示。- 適配過程:遍歷物件列表,檢查每個物件是否具有
play或dance方法。如果有,則建立一個Adapter例項,將相應的方法對映到organize_event方法。
裝飾器模式
裝飾器模式是一種結構性設計模式,允許在不影響其他物件的情況下,動態地為物件新增職責。
實際案例
在Web框架(如Django)中,裝飾器模式被廣泛用於實作諸如存取控制、快取控制、壓縮控制等跨領域關注點。
裝飾器實作
Python內建的裝飾器功能使得實作裝飾器模式變得非常容易。我們可以定義自己的裝飾器來擴充套件函式或類別的行為。
內容解密:
- 裝飾器的作用:裝飾器是一種可呼叫的物件(函式、方法或類別),它接受一個函式物件作為輸入並傳回另一個函式物件。
- Python裝飾器:Python內建的裝飾器功能允許我們以一種簡潔的方式擴充套件函式或類別的行為。
- 跨領域關注點:裝飾器模式適用於實作跨領域關注點,如資料驗證、快取、日誌記錄、監控、除錯、商業規則和加密等。
裝飾器模式(Decorator Pattern)實戰解析
在軟體開發中,效能最佳化是一項重要的任務。遞迴函式由於其簡潔性和易讀性而被廣泛使用,但往往伴隨著效能問題。本文將介紹如何利用裝飾器模式(Decorator Pattern)來最佳化遞迴函式的效能,特別是以記憶化(Memoization)技術為例。
樸素實作的問題
首先,讓我們考慮一個簡單的遞迴函式 number_sum(n),用於計算前 n 個數字的總和。以下是其樸素實作:
def number_sum(n):
if n == 0:
return 0
else:
return n + number_sum(n - 1)
if __name__ == "__main__":
from timeit import Timer
t = Timer("number_sum(50)", "from __main__ import number_sum")
print("Time: ", t.timeit())
內容解密:
- 此實作直接遞迴計算前
n個數字的總和。 - 當
n較大時,效能極差,因為存在大量重複計算。
執行上述程式碼會發現,當 n = 50 時,計算時間超過 7 秒,顯示出嚴重的效能問題。
引入記憶化技術
為瞭解決效能問題,我們引入記憶化技術,即將已經計算過的結果快取起來,避免重複計算。以下是修改後的程式碼:
sum_cache = {0: 0}
def number_sum(n):
if n in sum_cache:
return sum_cache[n]
res = n + number_sum(n - 1)
sum_cache[n] = res
return res
if __name__ == "__main__":
from timeit import Timer
t = Timer("number_sum(300)", "from __main__ import number_sum")
print("Time: ", t.timeit())
內容解密:
- 使用
sum_cache字典來儲存已經計算過的結果。 - 在計算前先檢查快取中是否存在結果,如果存在則直接傳回,否則進行計算並儲存結果。
- 大幅提升了效能,計算前 300 個數字的總和僅需約 0.13 秒。
裝飾器模式的最佳化
雖然記憶化技術解決了效能問題,但使得程式碼變得複雜。為此,我們可以使用裝飾器模式來簡化程式碼。以下是定義的 memoize 裝飾器:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def memoizer(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return memoizer
內容解密:
memoize裝飾器接受一個函式func作為輸入,並傳回一個包裝後的函式memoizer。memoizer函式檢查輸入引數args是否已經在快取cache中,如果存在則直接傳回快取結果,否則呼叫原始函式並儲存結果。- 使用
functools.wraps保留原始函式的檔案字串和簽名。
現在,我們可以輕鬆地將 memoize 裝飾器應用於任何需要記憶化的遞迴函式:
@memoize
def number_sum(n):
if n == 0:
return 0
else:
return n + number_sum(n - 1)
@memoize
def fibonacci(n):
if n in (0, 1):
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
內容解密:
- 使用
@memoize裝飾器簡化了記憶化的實作。 - 程式碼保持簡潔,同時獲得了最佳化的效能。
Bridge 模式:將抽象與實作分離
Bridge 模式是一種結構設計模式,旨在將抽象與其實作分離,使兩者可以獨立變化。在軟體開發中,這種模式特別有用,因為它允許我們在不影響客戶端的情況下更改或擴充套件實作。
現實世界的例子
在現代生活中,Bridge 模式的例子包括資訊產品(infoproduct)。資訊產品是一種線上資源,用於培訓、自我提升或商業發展。這些產品可以是 PDF 檔案、電子書、影片、線上課程或訂閱制的電子報等。
在軟體領域,Bridge 模式的例子包括:
- 裝置驅動程式:作業系統定義了裝置供應商必須實作的介面。
- 支付閘道:不同的支付閘道可以有不同的實作,但結帳流程保持一致。
使用 Bridge 模式的時機
當您想要在多個物件之間共用實作時,使用 Bridge 模式是一個好主意。這種模式可以幫助您定義:
- 適用於所有類別的抽象。
- 不同物件所涉及的介面。
實作 Bridge 模式
假設我們正在建構一個應用程式,用於管理和傳遞來自不同來源的內容。這些來源可以是:
- 網頁(根據 URL)。
- FTP 伺服器上的資源。
- 本地檔案系統上的檔案。
- 資料函式庫伺服器。
為了實作 Bridge 模式,我們首先定義了一個名為 ResourceContentFetcher 的介面,用於擷取內容:
class ResourceContentFetcher(Protocol):
def fetch(self, path: str) -> str:
...
接下來,我們定義了一個名為 ResourceContent 的抽象類別,用於表示資源內容。這個類別維護了一個對 ResourceContentFetcher 物件的參照:
class ResourceContent:
def __init__(self, imp: ResourceContentFetcher):
self._imp = imp
def get_content(self, path):
return self._imp.fetch(path)
然後,我們可以新增實作類別來擷取來自不同來源的內容,例如:
class URLFetcher:
def fetch(self, path):
res = ""
req = urllib.request.Request(path)
with urllib.request.urlopen(req) as response:
if response.code == 200:
res = response.read()
return res
class LocalFileFetcher:
def fetch(self, path):
with open(path) as f:
res = f.read()
return res
程式碼解密:
上述程式碼展示瞭如何使用 Bridge 模式來擷取來自不同來源的內容。ResourceContentFetcher 介面定義了擷取內容的方法,而 ResourceContent 抽象類別則維護了一個對該介面實作的參照。這使得我們可以輕鬆地新增新的內容擷取方式,而無需更改客戶端程式碼。
主函式與測試程式碼
最後,我們可以編寫一個主函式來測試 Bridge 模式的實作:
def main():
url_fetcher = URLFetcher()
rc = ResourceContent(url_fetcher)
res = rc.get_content("http://python.org")
print(f"Fetched content with {len(res)} characters")
localfs_fetcher = LocalFileFetcher()
rc = ResourceContent(localfs_fetcher)
pathname = os.path.abspath(__file__)
dir_path = os.path.split(pathname)[0]
path = os.path.join(dir_path, "file.txt")
res = rc.get_content(path)
print(f"Fetched content with {len(res)} characters")
程式碼解密:
上述程式碼展示瞭如何使用 Bridge 模式來擷取來自不同來源的內容。我們首先建立了一個 URLFetcher 物件和一個 LocalFileFetcher 物件,然後使用這些物件來擷取內容。最後,我們列印出擷取到的內容長度。
簡化複雜系統的門面模式
隨著系統演進,複雜度往往會大幅提升,進而形成大量且混雜的類別與互動關係。在多數情況下,我們並不希望將這種複雜性直接暴露給客戶端。此時,外觀(Facade)結構模式便能提供有效的解決方案。
外觀設計模式幫助我們隱藏系統內部的複雜性,並透過簡化的介導向客戶端暴露必要的功能。本質上,外觀是在現有複雜系統上實作的一層抽象。
以電腦為例
電腦是一種複雜的機器,其正常運作依賴於多個元件。為了簡化討論,這裡的「電腦」指根據馮·諾依曼架構的IBM相容機型。啟動電腦是一個極為複雜的過程:CPU、主記憶體和硬碟需要正常運作,引導程式需從硬碟載入至主記憶體,CPU須啟動作業系統核心等。與其將這些複雜過程直接暴露給客戶端,我們可以建立一個外觀(Facade),封裝整個流程,確保所有步驟按正確順序執行。
在物件導向設計與程式設計中,我們可能需要多個類別,但只需將Computer類別暴露給客戶端程式碼。客戶端只需執行Computer類別的start()方法,所有其他複雜部分都由外觀Computer類別負責處理。
真實世界的範例
外觀模式在現實生活中相當常見。當你撥打銀行或公司的客服電話時,通常首先會連線到客戶服務部門。客戶服務人員扮演了你與實際部門(如帳單、技術支援、一般協助等)之間的外觀角色,他們會協助處理你的特定問題。
另一個例子是,用於啟動汽車或機車的鑠匙也可被視為一種外觀。它提供了一個簡單的方式來啟動內部非常複雜的系統。當然,其他可以透過單一按鈕啟動的複雜電子裝置(如電腦)也是同樣的道理。
在軟體領域,django-oscar-datacash模組是一個與DataCash支付閘道整合的Django第三方模組。該模組具有一個閘道類別,提供了對各種DataCash API的精細存取。在此基礎上,它還提供了一個外觀類別,為那些不想處理細節的使用者提供了一個較不精細的API,並具備儲存交易以供稽核的功能。
Requests函式庫是另一個很好的外觀模式範例。它簡化了傳送HTTP請求和處理回應的過程,抽象化了HTTP協定的複雜性。開發人員可以輕鬆地發出HTTP請求,而無需處理底層Socket或HTTP方法的複雜細節。
外觀模式的使用案例
使用外觀模式最常見的原因是為複雜系統提供單一、簡單的入口點。透過引入外觀,客戶端程式碼可以透過簡單呼叫單一方法/函式來使用系統。同時,內部系統並未喪失任何功能,只是將其封裝起來。
不向客戶端程式碼暴露系統內部功能給我們帶來了額外的好處:我們可以對系統進行更改,但客戶端程式碼不受影響,無需對其進行任何修改。
如果系統有多個層級,外觀模式也很有用。我們可以為每個層級引入一個外觀入口點,讓所有層級透過其外觀相互通訊。這促進了鬆耦合,使各層級盡可能保持獨立。
實作外觀模式
假設我們想要使用多伺服器方法建立一個作業系統,類別似於MINIX 3或GNU Hurd的做法。多伺服器作業系統具有最小的核心,稱為微核心,它以特權模式執行。系統的所有其他服務都遵循伺服器架構(驅動程式伺服器、程式伺服器、檔案伺服器等)。每個伺服器屬於不同的記憶體位址空間,並在使用者模式下於微核心上執行。這種方法的優點是作業系統可以變得更加容錯、可靠和安全。例如,由於所有驅動程式都在使用者模式下於驅動程式伺服器上執行,驅動程式中的錯誤不會導致整個系統當機,也不會影響其他伺服器。這種方法的缺點是效能開銷和系統程式設計的複雜性,因為伺服器與微核心之間以及獨立伺服器之間的通訊是透過訊息傳遞實作的。訊息傳遞比Linux等單核心使用的分享記憶體模型更為複雜。
首先,我們定義一個Server介面。同時,使用一個列舉引數來描述伺服器的不同可能狀態。我們使用ABC技術禁止直接例項化Server介面,並使基本的boot()和kill()方法成為必須,假設不同的伺服器在啟動、終止和重新啟動時需要採取不同的動作。以下是這些元素的程式碼:
from enum import Enum
from abc import ABC, abstractmethod
State = Enum("State", "NEW RUNNING SLEEPING RESTART ZOMBIE")
class Server(ABC):
@abstractmethod
def __init__(self):
pass
def __str__(self):
return self.name
@abstractmethod
def boot(self):
pass
@abstractmethod
def kill(self, restart=True):
pass
內容解密:
State列舉定義了伺服器的五種可能狀態:新建(NEW)、執行中(RUNNING)、休眠(SLEEPING)、重新啟動(RESTART)和殭屍狀態(ZOMBIE)。Server類別是一個抽象基礎類別(ABC),定義了伺服器的基本介面,包括boot()和kill()方法。__str__方法傳回伺服器的名稱,便於列印伺服器資訊。boot()和kill()方法是抽象方法,需要在子類別中實作,用於控制伺服器的啟動和終止。
繼續實作檔案伺服器和程式伺服器
class FileServer(Server):
def __init__(self):
self.name = "FileServer"
self.state = State.NEW
def boot(self):
print(f"booting the {self}")
self.state = State.RUNNING
def kill(self, restart=True):
print(f"Killing {self}")
self.state = State.RESTART if restart else State.ZOMBIE
def create_file(self, user, name, perms):
msg = f"trying to create file '{name}' for user '{user}' with permissions {perms}"
print(msg)
內容解密:
FileServer繼承自Server,並實作了boot()和kill()方法,用於控制檔案伺服器的啟動和終止。create_file()方法用於在檔案伺服器上建立檔案,並列印相關資訊。- 狀態管理:在
boot()和kill()方法中更新伺服器的狀態,以反映其目前的運作狀態。
透過這種方式,外觀模式有效地簡化了複雜系統的使用介面,提高了系統的可維護性和擴充套件性。