隨著軟體專案規模的增長,管理複雜性和應對變化需求成為關鍵挑戰。本文以 Python 語言為基礎,介紹如何在實際專案中應用領域驅動設計和事件驅動架構等軟體架構模式。透過電子商務公司 MADE.com 的案例,展示如何利用軟體架構解決供應鏈管理的複雜問題,例如庫存最佳化和物流自動化。本文也探討如何結合測試驅動開發、領域模型、Repository、Service Layer 和 Unit of Work 等模式,並運用依賴反轉原則,建立一個更具彈性且易於維護的軟體系統。
為什麼需要軟體架構模式?
在軟體開發的世界中,隨著專案規模的擴大,管理複雜性和應對不斷變化的商業需求變得越來越重要。Harry Percival 和 Bob Gregory 在他們的書《Architecture Patterns with Python》中,分享了他們在 MADE.com 這個歐洲電子商務公司中,如何利用軟體架構模式來解決實際的商業問題。
管理複雜性與解決商業問題
MADE.com 經營著一個全球供應鏈,涉及貨運合作夥伴和製造商。為了降低成本,他們需要最佳化庫存交付時間,以避免貨物滯銷。理想情況下,客戶購買的沙發可以在當天到貨,並且可以直接運送到客戶家中,而無需儲存。這需要精確的時間安排,尤其是在貨物運輸需要三個月的情況下。在這個過程中,貨物可能會損壞、延遲、物流合作夥伴處理不當、檔案丟失、客戶更改訂單等問題頻頻出現。
為瞭解決這些問題,MADE.com 開發了智慧軟體來模擬現實世界中的業務操作,以實作業務流程的自動化。
為什麼選擇 Python?
Python 是目前世界上成長最快的程式語言之一,正在逐漸接近最受歡迎的程式語言榜首。然而,Python 社群才剛開始接觸 C# 和 Java 世界多年來一直在解決的問題。隨著新創公司成長為真正的企業,Web 應用程式和指令碼自動化正在演變成企業軟體。
Python 的禪語中提到:「應該有一種——最好只有一種——顯而易見的方法來做到這一點。」然而,隨著專案規模的擴大,最顯而易見的做法並不總是能夠幫助管理複雜性和不斷變化的需求。
介紹軟體架構模式
本文所討論的技術和模式並非全新,但對於 Python 社群來說大多是新鮮的。這些模式旨在幫助開發者管理複雜性、實作領域驅動設計(Domain-Driven Design, DDD)、以及構建事件驅動的微服務(Event-Driven Microservices)。
內容解密:
本段落主要介紹了軟體架構模式的重要性,以及 MADE.com 如何利用這些模式來解決實際的商業問題。關鍵點包括:
- 管理複雜性:隨著專案規模擴大,需要有效的軟體架構來管理複雜性。
- 解決商業問題:MADE.com 利用軟體架構模式來最佳化供應鏈管理,降低成本。
- Python 的優勢:Python 是一種流行的程式語言,但 Python 社群對於軟體架構模式的認識仍在發展中。
- 軟體架構模式的應用:介紹了領域驅動設計和事件驅動微服務等軟體架構模式,這些模式可以幫助開發者構建更具擴充套件性和維護性的系統。
使用Python實作領域驅動設計與事件驅動架構
在軟體開發領域,管理複雜性一直是開發者面臨的重要挑戰。為了應對這一挑戰,開發者通常會採用一些經典的架構模式,例如測試驅動開發(TDD)、領域驅動設計(DDD)和事件驅動架構。本文旨在介紹這些架構模式,並展示如何在Python中實作它們。
TDD、DDD和事件驅動架構的重要性
測試驅動開發(TDD):TDD是一種軟體開發過程,強調在編寫程式碼之前先編寫測試。它的好處包括提高程式碼的正確性、便於重構和新增功能而不必擔心迴歸問題。然而,如何充分利用測試是一個挑戰,包括確保測試執行速度快、覆寫率高等。
領域驅動設計(DDD):DDD是一種軟體開發方法,強調對業務領域的深入理解和建模。它的目標是建立一個能夠準確反映業務領域的模型。然而,如何避免模型被基礎設施問題所困擾是一個挑戰。
事件驅動架構:事件驅動架構是一種軟體架構模式,圍繞生產、檢測和消費事件以及對這些事件的反應來設計系統。它特別適用於微服務架構中各服務之間的整合。
內容解密:
- TDD幫助開發者編寫正確的程式碼並進行重構。
- DDD強調對業務領域的建模,以建立一個能夠準確反映業務需求的模型。
- 事件驅動架構提供了一種在微服務之間進行整合的有效方式。
本文的目標讀者
本文假設讀者已經接觸過一些複雜的Python應用程式,並對管理複雜性有所瞭解。即使讀者對DDD或經典的應用架構模式不熟悉,本文也將從基礎開始介紹。
本文的結構
本文分為兩個部分:
第一部分:建立支援領域建模的架構
- 領域建模和DDD(第1章和第7章):介紹如何建立一個與基礎設施無關的領域模型。
- Repository、Service Layer和Unit of Work模式(第2、4和5章):這三個模式有助於保持模型乾淨,避免外部依賴。
- 關於測試和抽象的思考(第3章和第6章):討論如何選擇抽象以及如何實作測試金字塔。
第二部分:事件驅動架構
- 事件驅動架構(第8-11章):介紹Domain Events、Message Bus和Handler模式,並展示如何使用事件進行微服務之間的整合。
- 命令查詢責任分離(第12章):展示命令查詢責任分離(CQRS)的例子。
- 依賴注入(第13章):討論如何管理和實作依賴注入。
如何使用本文
讀者可以跟隨本文的例子進行實踐。本文使用TDD的方式進行展示,先展示測試再展示實作。使用的技術包括Flask、SQLAlchemy、pytest、Docker和Redis等。
內容解密:
本文旨在提供一種在Python中實作TDD、DDD和事件驅動架構的方法。透過本文,讀者可以學習到如何建立一個乾淨、易於維護的軟體架構。
本文的學習方法與資源
本文的結構設計旨在透過單一範例專案來介紹不同的設計模式,讀者可以跟隨章節的進展逐步建立該專案。為了更深入地理解這些模式,讀者需要親自操作程式碼並體會其運作方式。所有的程式碼都可在 GitHub 上找到,每個章節都有對應的分支。
跟著書籍進行程式設計的三種方法
- 建立自己的儲存函式庫:嘗試根據書中的範例逐步建立應用程式,偶爾參考作者的儲存函式庫以取得提示。
- 將每個模式應用於自己的專案:嘗試將每個章節介紹的模式應用於自己的小型專案中,以驗證其是否適用於特定的使用場景。
- 完成每章節的練習:每個章節都提供了「讀者練習」的部分,讀者可以下載部分完成的程式碼並自行完成缺失的部分。
程式碼與授權
本文的程式碼和線上版本採用創用 CC BY-NC-ND 授權,允許讀者在非商業用途下自由複製和分享程式碼,但須註明出處。
本文使用的排版慣例
- 斜體字:表示新術語、網址、電子郵件地址、檔名和檔案副檔名。
等寬字型:用於程式列表,以及在段落中參照程式元素,如變數或函式名稱、資料函式庫、資料型別、環境變數、陳述式和關鍵字。等寬粗體:表示使用者應逐字輸入的命令或其他文字。等寬斜體字:表示應由使用者提供的值或由上下文決定的值所取代的文字。
提示與注意事項
- TIP:此元素表示提示或建議。
- NOTE:此元素表示一般性註解。
- WARNING:此元素表示警告或注意事項。
鳴謝
本文的作者在此對技術審閱者 David Seddon、Ed Jung 和 Hynek Schlawack 表示衷心的感謝。同時,也感謝早期釋放版本的讀者們提供的評論和建議,以及編輯 Corbin Collins 和製作人員的辛勤工作。
Python 之禪
執行 python -c "import this" 可顯示 Python 之禪,該哲學體現了 Python 程式設計的核心原則。
為何設計會失敗?
提到「混亂」這個詞時,你可能會想到吵鬧的股票交易所,或是早晨亂糟糟的廚房——一切都混淆不清。當你想到「秩序」這個詞時,或許會想到一間空曠的房間,寧靜而祥和。然而,對於科學家來說,混亂的特徵是同質性(一致性),而秩序的特徵則是複雜性(差異性)。
舉例來說,一個精心照顧的花園是一個高度有序的系統。園丁們透過小徑和籬笆來劃定界限,並標記出花壇或菜地。隨著時間的推移,花園不斷演變,變得更加豐富和茂盛;但如果沒有刻意的努力,花園就會變得荒蕪。雜草和野草會扼殺其他植物,覆寫小徑,直到最終每個部分看起來都一樣——野蠻而無序。
軟體系統同樣傾向於混亂。當我們剛開始構建一個新系統時,我們有遠大的理想,希望我們的程式碼能夠乾淨整潔、井然有序,但隨著時間的推移,我們發現它積累了雜亂無章的程式碼和邊緣案例,最終變成了一個令人困惑的管理類別和工具模組的混雜物。我們發現,原本合理的分層架構已經當機,就像一個過於濕軟的千層糕。混亂的軟體系統的特徵是功能上的同質性:API 處理程式具備領域知識並傳送電子郵件和執行日誌記錄;「業務邏輯」類別不進行計算但執行 I/O 操作;一切都與其他一切耦合在一起,因此更改系統的任何部分都變得危險重重。這種情況非常普遍,以至於軟體工程師們有自己的術語來描述這種混亂:大泥球反模式(Big Ball of Mud anti-pattern,如圖 P-1 所示)。
圖 P-1. 真實的依賴關係圖(來源:Alex Papadimoulis 的「企業依賴:大紗球」)
提示:
大泥球是軟體的自然狀態,就像荒野是你花園的自然狀態一樣。要防止當機,需要付出精力和方向。
幸運的是,避免建立大泥球的技術並不複雜。
封裝和抽象
封裝和抽象是我們作為程式設計師本能地採用的工具,即使我們不一定使用這些準確的詞彙。讓我們花一點時間來討論它們,因為它們是本文的重複背景主題。
封裝這個術語涵蓋了兩個密切相關的概念:簡化行為和隱藏資料。在這裡,我們使用的是第一個含義。透過識別程式碼中需要完成的任務,並將該任務交給一個定義明確的物件或函式,我們封裝了行為。我們稱這個物件或函式為抽象。
看看下面兩段 Python 程式碼:
使用 urllib 進行搜尋
import json
from urllib.request import urlopen
from urllib.parse import urlencode
params = dict(q='Sausages', format='json')
handle = urlopen('http://api.duckduckgo.com' + '?' + urlencode(params))
raw_text = handle.read().decode('utf8')
parsed = json.loads(raw_text)
results = parsed['RelatedTopics']
for r in results:
if 'Text' in r:
print(r['FirstURL'] + ' - ' + r['Text'])
使用 requests 進行搜尋
import requests
params = dict(q='Sausages', format='json')
parsed = requests.get('http://api.duckduckgo.com/', params=params).json()
results = parsed['RelatedTopics']
for r in results:
if 'Text' in r:
print(r['FirstURL'] + ' - ' + r['Text'])
內容解密:
這兩段程式碼都做同樣的事情:它們向 URL 提交表單編碼的值以使用搜尋引擎 API。但第二段程式碼更簡單易懂,因為它在更高的抽象層級上執行。第二段程式碼使用了 requests 函式庫,這使得傳送 HTTP 請求變得更加簡單。透過使用 requests.get() 方法並直接將查詢引數傳遞給它,我們避免了手動編碼 URL 和處理回應的複雜性。
我們可以進一步簡化這段程式碼,透過識別並命名我們希望程式碼為我們完成的任務,並使用更高層級的抽象使其明確:
使用 duckduckgo 模組進行搜尋
import duckduckgo
for r in duckduckgo.query('Sausages').results:
print(r.url + ' - ' + r.text)
內容解密:
這段程式碼使用了 duckduckgo 模組,這是一個更高層級的抽象,直接提供了搜尋結果。透過使用這個模組,我們可以避免處理底層的 HTTP 請求和 JSON 解析,使程式碼更加簡潔和易於理解。這種透過使用抽象來封裝行為的做法,是使程式碼更具表達力、更易測試和維護的有力工具。
分層架構
封裝和抽象幫助我們隱藏細節並保護資料的一致性,但我們還需要關注物件和函式之間的互動作用。當一個函式、模組或物件使用另一個時,我們說前者依賴於後者。這些依賴關係形成了一種網路或圖形。
在大泥球中,依賴關係是失控的(如圖 P-1 所示)。更改圖形中的一個節點變得困難,因為它有可能影響系統的其他許多部分。分層架構是解決這個問題的一種方法。在分層架構中,我們將程式碼分成不同的類別或角色,並引入規則,規定哪些類別的程式碼可以呼叫其他類別。
圖 P-2 展示了一個常見的三層架構。
圖 P-2. 分層架構
內容解密:
分層架構也許是構建業務軟體最常見的模式。在這個模型中,我們有使用者介面元件,可以是網頁、API 或命令列;這些使用者介面元件與包含業務規則和工作流程的業務邏輯層通訊;最後,我們有負責儲存和檢索資料的資料函式庫層。這種架構透過分離關注點,使系統更易於維護和擴充套件。
本文的其餘部分將系統地顛覆這個模型,遵循一個簡單的原則。
依賴反轉原則(Dependency Inversion Principle)
依賴反轉原則(DIP)可能是你已經熟悉的概念,因為它是SOLID原則中的D。SOLID原則是一套由Robert C. Martin提出的物件導向設計原則,包括單一職責原則、開放封閉原則、里氏替換原則、介面隔離原則和依賴反轉原則。
不幸的是,我們無法像封裝一樣,用三個簡短的程式碼範例來說明DIP。不過,本文的第一部分本質上是一個在整個應用程式中實施DIP的工作範例,因此你將獲得足夠的具體範例。
DIP的形式化定義
- 高層模組不應該依賴於低層模組。兩者都應該依賴於抽象。
- 抽象不應該依賴於細節。相反,細節應該依賴於抽象。
高層模組與低層模組
高層模組是你所在組織真正關心的程式碼。例如,如果你在製藥公司工作,你的高層模組可能涉及患者和試驗資料。如果你在銀行工作,你的高層模組可能涉及交易和交換。軟體系統中的高層模組是處理現實世界概念的功能、類別和套件。
相對地,低層模組是你所在組織不太關心的程式碼。你的HR部門不太可能對檔案系統或網路通訊端感興趣。你也不太可能與財務團隊討論SMTP、HTTP或AMQP。對於非技術利益相關者來說,這些低層概念並不有趣或相關。他們只關心高層概念是否正確運作。如果薪水能準時發放,你的業務就不太會在乎這是由cron任務還是Kubernetes上的暫時函式執行的。
依賴關係
所謂的「依賴」,並不一定意味著匯入或呼叫,而是一種更廣泛的概念,即一個模組瞭解或需要另一個模組。
抽象介面
我們已經提到了抽象:它們是封裝行為的簡化介面,就像我們的duckduckgo模組封裝了搜尋引擎的API一樣。
所有電腦科學中的問題都可以透過增加一層間接性來解決。 — David Wheeler
為什麼需要DIP?
DIP的第一部分指出,我們的業務程式碼不應該依賴於技術細節;相反,兩者都應該使用抽象。這樣做的主要原因是,我們希望能夠獨立地更改它們。高層模組應該能夠根據業務需求輕鬆更改。低層模組(細節)在實踐中往往更難更改。例如,思考一下重構以更改函式名稱與定義、測試和佈署資料函式庫遷移以更改列名。我們不希望業務邏輯的更改因為與低層基礎設施細節緊密耦合而變慢。同樣,重要的是能夠在需要時更改基礎設施細節(例如,對資料函式庫進行分片),而無需對業務層進行更改。在它們之間新增抽象(著名的額外間接層)允許兩者(更)獨立地更改。
領域模型:業務邏輯的歸屬地
在我們能夠顛覆三層架構之前,我們需要更多地討論中間層:高層模組或業務邏輯。我們的設計出錯的最常見原因之一是,業務邏輯分散在應用程式的各個層中,使得它難以識別、理解和更改。
第1章展示瞭如何使用領域模型模式構建業務層。本文第一部分的其餘模式展示瞭如何透過選擇正確的抽象並持續應用DIP,使領域模型易於更改且不受低層問題的影響。
構建支援領域建模的架構
大多數開發人員從未見過領域模型,只見過資料模型。 — Cyrille Martraire, DDD EU 2017
我們與之討論架構的大多數開發人員都有種感覺,覺得事情可以做得更好。他們經常試圖拯救一個出了問題的系統,並試圖將一些結構重新引入一團亂麻中。他們知道自己的業務邏輯不應該分散在各處,但不知道如何修復它。
我們發現,許多開發人員在被要求設計一個新系統時,會立即開始構建資料函式庫架構,而將物件模型視為事後才想到的。這就是一切開始出錯的地方。相反,行為應該放在首位,並驅動我們的儲存需求。畢竟,我們的客戶並不在乎資料模型。他們關心的是系統的功能;否則他們就會使用試算表。
本文的第一部分將探討如何透過TDD(在第1章中)構建豐富的物件模型,然後我們將展示如何保持該模型與技術問題解耦。我們將展示如何構建永續性無知的程式碼,以及如何在我們的領域周圍建立穩定的API,以便我們能夠積極地重構。
為此,我們提出了四種關鍵的設計模式:
- Repository模式:對永續性儲存概念的抽象
- Service Layer模式:清楚定義我們的使用案例開始和結束的位置
- Unit of Work模式:提供原子操作
- Aggregate模式:強制執行我們資料的完整性
如果你想了解我們要去哪裡,請看一下圖I-1,但如果它現在還沒有意義,請不要擔心!我們將在本文的這一部分中逐一介紹圖中的每個框。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Python 軟體架構模式 領域驅動設計
package "機器學習流程" {
package "資料處理" {
component [資料收集] as collect
component [資料清洗] as clean
component [特徵工程] as feature
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml