結構化設計模式著重於簡化介面、增強物件功能,並降低系統複雜度。Builder 模式能抽象化複雜物件的初始化過程,適用於框架、函式庫和 API 設計。Adapter 模式則解決介面不相容的問題,透過繼承或組合方式橋接不同物件。行為設計模式則關注物件互動和執行邏輯。責任鏈模式讓請求沿著處理者鏈傳遞,直到被處理或拒絕,適用於日誌解析等場景。範本方法模式定義演算法骨架,子類別可自定義特定步驟,提升程式碼重用性。鏈式模式結合範本方法模式,更能展現事件處理的靈活性和擴充套件性,例如根據事件型別動態調整處理順序,實作更精細的控制流程。
結構化設計模式在軟體開發中的應用
在軟體開發領域中,設計模式是一種被廣泛接受的解決特定問題的方法。結構化設計模式(Structural patterns)是其中一種重要的類別,它們幫助開發者建立更簡單、更強大的介面,或透過擴充套件功能而不增加複雜度來增強物件。
Builder 模式
Builder 模式是一種有趣的設計模式,它抽象了物件複雜的初始化過程。這種模式不依賴於語言的特殊性,因此在 Python 或其他語言中同樣適用。Builder 模式通常用於框架、函式庫或 API 的設計,因為它能夠簡化複雜物件的建立過程。
實作 Builder 模式
假設我們需要建立一個複雜的物件,這個物件需要多個其他物件才能正常運作。與其讓使用者自己建立所有輔助物件並將它們分配給主物件,我們可以使用 Builder 模式。Builder 物件知道如何建立所有部分並將它們連結在一起,提供一個介面(可以是類別方法)來引數化結果物件的所有資訊。
結構化設計模式的優勢
結構化設計模式在需要建立更簡單的介面或更強大的物件時非常有用。透過擴充套件功能而不增加複雜度,這些模式幫助開發者建立更豐富的物件。
Adapter 模式
Adapter 模式,也稱為 Wrapper,是一種簡單卻非常有用的設計模式。它解決了兩個或多個物件介面不相容的問題。
實作 Adapter 模式
當我們的程式碼與某個模型或類別集合作時,通常會遇到介面不相容的問題。例如,如果有多個物件具有 fetch() 方法,我們希望保持這個介面以避免對程式碼進行重大更改。但是,當我們需要新增新的資料來源時,如果這個新的資料來源沒有 fetch() 方法,就會出現問題。
為瞭解決這個問題,我們可以使用 Adapter 模式。Adapter 模式有兩種實作方式:繼承和組合。
使用繼承實作 Adapter 模式
from _adapter_base import UsernameLookup
class UserSource(UsernameLookup):
def fetch(self, user_id, username):
user_namespace = self._adapt_arguments(user_id, username)
return self.search(user_namespace)
@staticmethod
def _adapt_arguments(user_id, username):
return f"{user_id}:{username}"
使用組合實作 Adapter 模式
class UserSource:
def __init__(self, username_lookup):
self.username_lookup = username_lookup
def fetch(self, user_id, username):
user_namespace = self._adapt_arguments(user_id, username)
return self.username_lookup.search(user_namespace)
@staticmethod
def _adapt_arguments(user_id, username):
return f"{user_id}:{username}"
選擇適當的 Adapter 模式實作方式
雖然繼承可以實作 Adapter 模式,但它會帶來更多的耦合和不靈活性。因此,組合通常是更好的選擇。
使用 __getattr__() 方法實作通用 Adapter
如果需要適應多個方法,並且可以設計出適應其簽名的通用方法,那麼可以使用 __getattr__() 魔術方法將請求重定向到包裝的物件。但是,應該小心避免增加解決方案的複雜度。
裝飾器(Decorator)與組合(Composite)設計模式在軟體開發中的應用
在軟體開發領域中,設計模式扮演著至關重要的角色,它們提供了一套成熟的解決方案來應對常見的軟體設計問題。本篇文章將探討兩種重要的設計模式:裝飾器(Decorator)模式和組合(Composite)模式,並透過具體的程式碼範例來說明它們在實際開發中的應用。
組合(Composite)模式
組合模式是一種結構型設計模式,它允許我們將物件組合成樹狀結構,以表示部分與整體的層次關係。這種模式使得客戶端可以統一地處理單個物件和組合物件。
實際應用範例:購物車系統
假設我們正在開發一個購物車系統,其中包含單個商品和商品組合。單個商品具有固定的價格,而商品組合則包含多個商品或子組合,並且可以對整個組合應用折扣。
class Product:
def __init__(self, name: str, price: float) -> None:
self._name = name
self._price = price
@property
def price(self):
return self._price
class ProductBundle:
def __init__(
self,
name: str,
perc_discount: float,
*products: Iterable[Union[Product, "ProductBundle"]]
) -> None:
self._name = name
self._perc_discount = perc_discount
self._products = products
@property
def price(self) -> float:
total = sum(p.price for p in self._products)
return total * (1 - self._perc_discount)
內容解密:
Product類別代表單個商品,具有price屬性。ProductBundle類別代表商品組合,可以包含多個Product或其他ProductBundle,並對整個組合應用折扣。- 透過使用組合模式,我們可以統一處理單個商品和商品組合,使得計算總價格的邏輯更加簡潔和易於維護。
裝飾器(Decorator)模式
裝飾器模式是一種結構型設計模式,它允許我們在不修改原有物件結構的情況下,動態地為物件新增新的行為或功能。
實際應用範例:查詢物件的動態增強
假設我們有一個查詢物件 DictQuery,它根據提供的引數構建一個字典形式的查詢。我們希望能夠動態地對查詢結果進行各種轉換,例如移除空值、轉換為小寫等。
class DictQuery:
def __init__(self, **kwargs):
self._raw_query = kwargs
def render(self) -> dict:
return self._raw_query
class QueryEnhancer:
def __init__(self, query: DictQuery):
self.decorated = query
def render(self):
return self.decorated.render()
class RemoveEmpty(QueryEnhancer):
def render(self):
original = super().render()
return {k: v for k, v in original.items() if v}
class CaseInsensitive(QueryEnhancer):
def render(self):
original = super().render()
return {k: v.lower() for k, v in original.items()}
內容解密:
DictQuery類別代表基本的查詢物件,可以渲染出查詢字典。QueryEnhancer類別是裝飾器的基礎類別,它接收一個DictQuery物件並實作相同的render方法。RemoveEmpty和CaseInsensitive是具體的裝飾器類別,分別實作了移除空值和轉換為小寫的功能。- 透過使用裝飾器模式,我們可以動態地組合多個裝飾器來增強查詢物件的功能,而無需修改原有的
DictQuery類別。
行為設計模式:簡化物件互動的藝術
在軟體開發中,物件之間的互動與合作是構建可維護系統的關鍵。行為設計模式提供了一套解決方案,用於定義物件之間的溝通方式、介面以及執行時的互動邏輯。本文將探討幾種重要的行為設計模式,包括責任鏈模式、範本方法模式、命令模式和狀態模式,並透過例項展示如何運用這些模式來提升程式碼的品質。
責任鏈模式(Chain of Responsibility)
責任鏈模式是一種行為設計模式,允許將請求沿著處理者鏈進行傳遞,直到其中一個處理者對請求做出回應或請求被拒絕。該模式降低了請求傳送者和接收者之間的耦合度。
例項:日誌事件解析
假設我們需要從日誌檔案中解析事件資訊,並將其轉換為結構化的資料。我們可以定義一個處理者鏈,每個處理者負責解析日誌中的特定資訊。
from abc import ABC, abstractmethod
class LogHandler(ABC):
def set_next(self, handler):
self.next_handler = handler
return handler
@abstractmethod
def handle(self, log_line):
pass
class ErrorLogHandler(LogHandler):
def handle(self, log_line):
if "ERROR" in log_line:
# 解析錯誤日誌
print("Error Log:", log_line)
elif hasattr(self, 'next_handler'):
self.next_handler.handle(log_line)
class InfoLogHandler(LogHandler):
def handle(self, log_line):
if "INFO" in log_line:
# 解析資訊日誌
print("Info Log:", log_line)
elif hasattr(self, 'next_handler'):
self.next_handler.handle(log_line)
# 使用範例
error_handler = ErrorLogHandler()
info_handler = InfoLogHandler()
error_handler.set_next(info_handler)
log_lines = [
"2023-03-01 ERROR Something went wrong",
"2023-03-01 INFO Server started successfully"
]
for log_line in log_lines:
error_handler.handle(log_line)
內容解密:
LogHandler是一個抽象基礎類別,定義了處理者介面和鏈式處理的邏輯。ErrorLogHandler和InfoLogHandler是具體處理者,分別負責解析錯誤和資訊日誌。- 在
handle方法中,每個處理者檢查是否能夠處理給定的日誌行,如果不能,則將請求傳遞給鏈中的下一個處理者。
範本方法模式(Template Method)
範本方法模式定義了一個演算法的骨架,將某些步驟的實作延遲到子類別中。
命令模式(Command)
命令模式將請求封裝為一個物件,從而允許引數化客戶端請求、佇列或記錄請求日誌,並支援可復原的操作。
狀態模式(State)
狀態模式允許物件在其內部狀態改變時改變其行為,物件看起來似乎修改了其類別。
鏈式模式(Chain of Responsibility Pattern)與範本方法(Template Method Pattern)在事件處理中的應用
在前面的實作中,我們達成了一個有趣的解決方案,該方案符合開閉原則(open/closed principle),並依賴於使用 __subclasses__() 魔法方法來發現所有可能的事件型別,並使用正確的事件處理資料,透過封裝在每個類別中的方法解析責任。然而,這個設計模式將帶來額外的好處。
鏈式模式的實作
在這個實作中,我們將以稍微不同的方式建立事件。每個事件仍然具有確定是否可以處理特定日誌行的邏輯,但它還將具有一個後繼者(successor)。這個後繼者是一個新的事件,是鏈中的下一個事件,它將在第一個事件無法處理文字行的情況下繼續處理該文字行。邏輯很簡單——我們將事件連結起來,每個事件都嘗試處理資料。如果它可以處理,則傳回結果。如果不能,則將其傳遞給其後繼者並重複此過程,如下面的程式碼所示:
import re
from typing import Optional, Pattern
class Event:
pattern: Optional[Pattern[str]] = None
def __init__(self, next_event=None):
self.successor = next_event
def process(self, logline: str):
if self.can_process(logline):
return self._process(logline)
if self.successor is not None:
return self.successor.process(logline)
def _process(self, logline: str) -> dict:
parsed_data = self._parse_data(logline)
return {
"type": self.__class__.__name__,
"id": parsed_data["id"],
"value": parsed_data["value"],
}
@classmethod
def can_process(cls, logline: str) -> bool:
return cls.pattern is not None and cls.pattern.match(logline)
@classmethod
def _parse_data(cls, logline: str) -> dict:
if not cls.pattern:
return {}
if (parsed := cls.pattern.match(logline)) is not None:
return parsed.groupdict()
return {}
class LoginEvent(Event):
pattern = re.compile(r"(?P<id>\d+):\s+login\s+(?P<value>\S+)")
class LogoutEvent(Event):
pattern = re.compile(r"(?P<id>\d+):\s+logout\s+(?P<value>\S+)")
內容解密:
Event類別的設計:Event類別是所有事件的基礎類別,它定義了事件處理的基本邏輯,包括process方法和_process方法。process方法用於檢查當前事件是否能夠處理給定的日誌行,如果可以,則呼叫_process方法進行處理;否則,將處理責任轉交給鏈中的下一個事件(即successor)。can_process類別方法:用於判斷給定的日誌行是否能夠被當前事件類別處理。它檢查類別屬性pattern是否與日誌行匹配。_parse_data類別方法:負責解析日誌行並提取相關資料。它使用pattern屬性進行匹配,並傳回一個包含提取資料的字典。LoginEvent和LogoutEvent類別:這兩個類別分別代表登入和登出事件,它們透過定義不同的正規表示式模式來匹配特定的日誌行。
透過這種方式,我們建立了事件物件,並按照特定的順序排列它們。由於它們都具有 process() 方法,因此它們對於這個訊息是多型的,所以它們被排列的順序對於客戶端來說是完全透明的。
鏈式模式的靈活性
這個解決方案足夠靈活,並且與我們之前的實作分享一個有趣的特性——所有條件都是相互排斥的。只要沒有衝突,並且沒有資料片段有多個處理程式,那麼以任何順序處理事件都不會成為問題。
然而,如果我們無法做出這樣的假設,那麼我們可以透過動態地組裝鏈來實作這樣的需求。例如,我們可以新增一個通用型別來分組登入和登出會話事件,如下面的程式碼所示:
class SessionEvent(Event):
pattern = re.compile(r"(?P<id>\d+):\s+log(in|out)\s+(?P<value>\S+)")
現在,如果我們想在登入事件之前捕捉這個通用會話事件,可以透過以下鏈來實作:
chain = SessionEvent(LoginEvent(LogoutEvent()))
透過改變順序,我們可以說通用會話事件具有比登入事件更高的優先順序,但不是登出事件,等等。
範本方法模式
範本方法模式是一種在正確實作時產生重要好處的模式。它主要允許我們重用程式碼,並使我們的物件在保留多型性的同時更加靈活和易於更改。
範本方法模式的核心思想
範本方法模式的核心思想是,在一個類別層次結構中定義一些行為,比如其公共介面中的一個重要方法。所有類別都實作這個方法,但它們可以根據需要定製某些步驟。
內容解密:
範本方法模式的好處:範本方法模式允許程式碼重用,並使物件更加靈活。它透過在基礎類別中定義演算法的骨架,並允許子類別重寫某些步驟來實作這一點。
實作範本方法模式:要實作範本方法模式,需要在基礎類別中定義一個範本方法,該方法呼叫其他方法來執行特定的操作。子類別可以根據需要重寫這些方法。
透過使用鏈式模式和範本方法模式,我們可以建立更加靈活和可擴充套件的事件處理系統。這些模式不僅提高了程式碼的可重用性,也使得系統更容易維護和擴充套件。