Python 的動態特性和語言機制簡化了許多設計模式的實作,使開發者能更彈性地處理物件建立。本文將探討工廠模式和建造者模式,並以實際案例說明如何在 Python 中活用這些模式,同時也分析其優缺點及適用場景。工廠模式範例中,我們將示範如何根據檔案型別(JSON 或 XML)動態選擇對應的資料提取器,達成程式碼的簡潔和可擴充套件性。建造者模式範例則以披薩訂購系統為例,展示如何逐步構建一個包含多種組態選項的複雜物件,同時保持程式碼的清晰和易維護性。
工廠方法模式的應用
工廠方法模式是一種建立型設計模式,旨在解決物件建立和追蹤的問題。當應用程式中的物件建立程式碼分散在多個地方時,很難追蹤和管理這些物件。工廠方法模式透過集中物件建立的邏輯,使得物件的追蹤和管理變得更加容易。
使用工廠方法模式的場景
- 集中物件建立:當需要建立多個相關的物件時,工廠方法模式可以幫助集中建立邏輯,避免分散的建立程式碼。
- 解耦物件建立和物件使用:工廠方法模式使得物件建立和物件使用之間的耦合度降低,提高了程式碼的靈活性和可維護性。
- 改善效能和記憶體使用:透過控制物件的建立,工廠方法模式可以改善應用程式的效能和記憶體使用。
實作工廠方法模式
以下是一個使用工廠方法模式來解析 JSON 和 XML 檔案的例子。
首先,定義兩個資料提取器類別:JSONDataExtractor
和 XMLDataExtractor
。
import json
import xml.etree.ElementTree as ET
from pathlib import Path
class JSONDataExtractor:
def __init__(self, filepath: Path):
self.data = {}
with open(filepath) as f:
self.data = json.load(f)
@property
def parsed_data(self):
return self.data
class XMLDataExtractor:
def __init__(self, filepath: Path):
self.tree = ET.parse(filepath)
@property
def parsed_data(self):
return self.tree
內容解密:
JSONDataExtractor
類別用於從 JSON 檔案中提取資料。它使用json.load()
方法將 JSON 檔案解析成 Python 物件。XMLDataExtractor
類別用於從 XML 檔案中提取資料。它使用xml.etree.ElementTree.parse()
方法將 XML 檔案解析成 ElementTree 物件。- 兩個類別都提供了
parsed_data
屬性,用於取得解析後的資料。
然後,定義一個工廠函式 extract_factory
,根據檔案副檔名選擇合適的資料提取器類別。
def extract_factory(filepath: Path):
ext = filepath.name.split(".")[-1]
if ext == "json":
return JSONDataExtractor(filepath)
elif ext == "xml":
return XMLDataExtractor(filepath)
else:
raise ValueError("無法提取資料")
內容解密:
extract_factory
函式根據檔案的副檔名決定傳回哪個資料提取器類別的例項。- 如果檔案副檔名是 “json”,則傳回
JSONDataExtractor
的例項。 - 如果檔案副檔名是 “xml”,則傳回
XMLDataExtractor
的例項。 - 如果檔案副檔名不被支援,則丟擲
ValueError
例外。
工廠模式(Factory Pattern)實務解析
工廠模式是一種常見的設計模式,主要用於根據不同的輸入條件建立不同的物件。在本章中,我們將探討工廠模式的原理、實作方式以及在Python中的應用。
工廠方法模式實作範例
首先,我們定義了一個名為extract
的主要函式,用於處理不同型別的資料檔案。以下為處理JSON檔案的部分程式碼:
def extract(case: str):
dir_path = Path(__file__).parent
if case == "json":
path = dir_path / Path("movies.json")
factory = extract_factory(path)
data = factory.parsed_data
for movie in data:
print(f"- {movie['title']}")
director = movie["director"]
if director:
print(f" 導演:{director}")
genre = movie["genre"]
if genre:
print(f" 型別:{genre}")
內容解密:
extract
函式根據case
引數決定處理的檔案型別。- 使用
extract_factory
函式建立對應的資料提取器例項。 - 對於JSON檔案,解析後的資料儲存在
data
變數中,並逐一列印電影標題、導演和型別。
接著,我們增加了處理XML檔案的程式碼,使用XPath查詢特定元素並輸出相關資訊:
elif case == "xml":
path = dir_path / Path("person.xml")
factory = extract_factory(path)
data = factory.parsed_data
search_xpath = ".//person[lastName='Liar']"
items = data.findall(search_xpath)
for item in items:
first = item.find("firstName").text
last = item.find("lastName").text
print(f"- {first} {last}")
for pn in item.find("phoneNumbers"):
pn_type = pn.attrib["type"]
pn_val = pn.text
phone = f"{pn_type}: {pn_val}"
print(f" {phone}")
內容解密:
- 使用XPath表示式
.//person[lastName='Liar']
查詢姓氏為"Liar"的人員元素。 - 輸出符合條件的人員姓名及電話號碼資訊。
最後,我們增加了測試程式碼以驗證實作的正確性:
if __name__ == "__main__":
print("* JSON案例 *")
extract(case="json")
print("* XML案例 *")
extract(case="xml")
內容解密:
- 在主程式中分別呼叫
extract
函式處理JSON和XML檔案。 - 列印輸出結果以驗證程式的正確性。
工廠模式的優缺點分析
工廠模式的主要優點在於它能夠封裝物件建立的邏輯,使得程式碼更加靈活和可擴充套件。然而,在Python中,由於其動態型別和一級函式的特性,工廠模式可能會被視為過度設計。
是否應該使用工廠方法模式?
經驗豐富的Python開發者經常批評工廠方法模式過於複雜。Python的動態特性和語言特性(如預設引數和關鍵字引數)使得直接建立物件或使用簡單函式成為更直接的解決方案。
抽象工廠模式簡介
抽象工廠模式是工廠方法模式的泛化,它將多個工廠方法組織在一起,每個工廠方法負責生成不同型別的物件。我們將在後續章節中探討抽象工廠模式的具體應用和實作方式。
抽象工廠模式在遊戲開發中的應用
抽象工廠模式是一種建立型設計模式,它提供了一種方法來封裝一組具有相同主題的單獨工廠的建立過程。在遊戲開發中,這種模式可以用來建立不同型別的遊戲世界,例如兒童遊戲和成人遊戲。
現實世界的例子
抽象工廠模式在汽車製造業中得到了應用。同樣的機器裝置可以用於沖壓不同車型的零件(門、面板、引擎蓋、翼子板和鏡子)。在軟體開發領域,factory_boy
套件(https://github.com/FactoryBoy/factory_boy)和 model_bakery
套件(https://github.com/model-bakers/model_bakery)提供了抽象工廠的實作,用於在測試中建立 Django 模型。
抽象工廠模式的應用場景
由於抽象工廠模式是工廠方法模式的概括,因此它提供了相同的優點:使跟蹤物件建立變得更容易,將物件建立與物件使用分離,並有可能改善應用程式的記憶體使用和效能。
實作抽象工廠模式
讓我們透過一個例子來演示抽象工廠模式。假設我們正在開發一個遊戲,或者想要在我們的應用程式中新增一個小遊戲來娛樂使用者。我們希望至少包含兩個遊戲,一個適合兒童,一個適合成人。我們將根據使用者輸入在執行時決定建立哪個遊戲。抽象工廠負責遊戲的建立部分。
兒童遊戲:FrogWorld
首先,我們來看看兒童遊戲 FrogWorld
。遊戲的主角是一隻青蛙,它喜歡吃蟲子。每個英雄都需要一個好名字,在我們的例子中,名字由使用者在執行時提供。interact_with()
方法用於描述青蛙與障礙物(例如蟲子、謎題和其他青蛙)的互動。
class Frog:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f"{self} the Frog encounters {obstacle} and {act}!"
print(msg)
class Bug:
def __str__(self):
return "a bug"
def action(self):
return "eats it"
class FrogWorld:
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return "\n\n\t------ Frog World -------"
def make_character(self):
return Frog(self.player_name)
def make_obstacle(self):
return Bug()
成人遊戲:WizardWorld
WizardWorld
遊戲與 FrogWorld
相似,但巫師與怪物(如獸人)戰鬥,而不是吃蟲子。
class Wizard:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
def interact_with(self, obstacle):
act = obstacle.action()
msg = f"{self} the Wizard battles against {obstacle} and {act}!"
print(msg)
class Ork:
def __str__(self):
return "an evil ork"
def action(self):
return "kills it"
class WizardWorld:
def __init__(self, name):
print(self)
self.player_name = name
def __str__(self):
return "\n\n\t------ Wizard World -------"
def make_character(self):
return Wizard(self.player_name)
def make_obstacle(self):
return Ork()
GameEnvironment 類別
GameEnvironment
類別是遊戲的入口點。它接受工廠作為輸入,並使用它來建立遊戲世界。play()
方法啟動了建立的英雄與障礙物之間的互動。
class GameEnvironment:
def __init__(self, factory):
self.hero = factory.make_character()
self.obstacle = factory.make_obstacle()
def play(self):
self.hero.interact_with(self.obstacle)
validate_age() 函式和 main() 函式
validate_age()
函式提示使用者輸入有效的年齡。如果年齡無效,則傳回一個元組,第一個元素設定為 False。如果年齡有效,則第一個元素設定為 True。
def validate_age(name):
age = None
try:
age_input = input(f"Welcome {name}. How old are you? ")
age = int(age_input)
except ValueError:
print(f"Age {age} is invalid, please try again...")
return False, age
return True, age
def main():
name = input("Hello. What's your name? ")
valid_input = False
while not valid_input:
valid_input, age = validate_age(name)
game = FrogWorld if age < 18 else WizardWorld
environment = GameEnvironment(game(name))
environment.play()
if __name__ == "__main__":
main()
#### 內容解密:
- 在這個例子中,我們定義了兩個遊戲世界:
FrogWorld
和WizardWorld
。 - 每個遊戲世界都有自己的角色和障礙物類別。
GameEnvironment
類別使用抽象工廠模式來建立遊戲世界。validate_age()
函式用於驗證使用者輸入的年齡。main()
函式是程式的入口點,它根據使用者的年齡決定要玩哪個遊戲。
建造者模式(Builder Pattern)深度解析
建造者模式是一種建立型設計模式,主要用於解決複雜物件的逐步建立問題。它透過將物件的構建過程與其表示分離,使得相同的構建過程可以用來建立不同的表示。
現實世界範例
在日常生活中,建造者模式被廣泛應用於快餐店的食品製作流程中。無論是製作經典漢堡還是芝士漢堡,製作過程都是相同的,但最終的產品卻有所不同。這裡,餐廳的收銀員充當了導演的角色,而製作食品的員工則是具體的建造者。
在軟體開發領域,django-query-builder 函式庫就是一個典型的建造者模式應用範例。它允許開發者動態地構建 SQL 查詢陳述式,從而滿足不同的查詢需求。
與工廠模式的比較
建造者模式與工廠模式的主要區別在於:工廠模式是一步到位地建立物件,而建造者模式則是透過多個步驟來建立物件,並且通常需要一個導演來協調整個過程。此外,工廠模式會立即傳回建立好的物件,而建造者模式則需要客戶端程式碼明確要求導演傳回最終的物件。
建造者模式的使用場景
當一個物件需要透過多種可能的組態來構建時,建造者模式就顯得特別有用。典型的情況包括:一個類別有多個建構函式,且引數數量不同,這往往會導致程式碼混亂或容易出錯。
此外,當物件的構建過程比簡單地設定初始值更為複雜時,建造者模式也能發揮重要作用。例如,如果物件的完整建立涉及多個步驟,如引數驗證、資料結構設定或甚至呼叫外部服務等,建造者模式可以封裝這種複雜性。
實作建造者模式
以下是一個使用建造者模式來製作披薩訂購應用程式的範例。這個範例非常有趣,因為製作披薩需要按照特定的順序進行多個步驟。
首先,我們匯入所需的模組並宣告一些 Enum 引數和一個常數,用於在應用程式中多次使用的 STEP_DELAY
常數,用於在準備披薩的不同步驟之間新增時間延遲。
import time
from enum import Enum
PizzaProgress = Enum("PizzaProgress", "queued preparation baking ready")
PizzaDough = Enum("PizzaDough", "thin thick")
PizzaSauce = Enum("PizzaSauce", "tomato creme_fraiche")
PizzaTopping = Enum("PizzaTopping", "mozzarella double_mozzarella bacon ham mushrooms red_onion oregano")
# 延遲時間(秒)
STEP_DELAY = 3
我們的最終產品是一個披薩,由 Pizza
類別描述。在使用建造者模式時,最終產品的類別通常非常簡單,因為它不應該被直接例項化。建造者負責建立最終產品的例項,並確保它被正確準備。
class Pizza:
def __init__(self, name):
self.name = name
self.dough = None
self.sauce = None
self.topping = []
def __str__(self):
return self.name
def prepare_dough(self, dough):
self.dough = dough
print(f"Preparing {dough} dough for {self.name}...")
內容解密:
Pizza
類別初始化時設定披薩名稱,並將麵團、醬料和配料初始化為預設值。prepare_dough
方法用於準備披薩麵團,這裡直接在Pizza
類別中定義,以展示最終產品類別也可以具有某些責任。- 使用 Enum 來定義披薩的不同狀態、麵團型別、醬料型別和配料型別,以提高程式碼的可讀性和可維護性。
建造者模式的優點
- 將複雜物件的構建過程與其表示分離,提高了程式碼的靈活性和可重用性。
- 可以透過相同的構建過程來建立不同的表示。
- 有助於封裝複雜的構建邏輯,提高程式碼的可讀性和可維護性。