領域驅動設計(DDD)是一種以領域模型為核心的軟體開發方法,它強調對業務領域的深入理解和模型的準確性。在 Python 中實作 DDD 可以有效提升程式碼的可讀性、可維護性和可測試性。本文將以庫存分配案例為例,逐步講解如何使用 Python 的特性,如 dataclass 和 NamedTuple,來實作 DDD 的核心概念,包括值物件、實體和領域服務。同時,也將示範如何運用測試驅動開發(TDD)來確保模型的正確性。透過本文,讀者可以學習如何將 DDD 的原則應用於實際的 Python 專案中,建構更具彈性且易於演進的軟體系統。
內容解密:
此圖示顯示了應用程式的不同元件之間的互動關係。應用程式首先與Service Layer互動,由Service Layer來控制業務邏輯的流程。Service Layer會與Repository互動,以進行資料存取操作。同時,Service Layer也會使用Unit of Work來確保操作的原子性。此外,應用程式也會直接與領域模型互動,領域模型代表了業務邏輯的核心。領域模型的變更會影響到Service Layer的實作。
程式碼範例
class Repository:
def __init__(self, session):
self.session = session
def get(self, id):
return self.session.query(MyModel).get(id)
def add(self, model):
self.session.add(model)
#### 內容解密:
這段程式碼定義了一個Repository類別,用於封裝資料存取邏輯。它使用了SQLAlchemy的session物件來進行資料函式庫操作。get方法用於根據id檢索特定的MyModel例項,而add方法用於將新的MyModel例項新增到session中。這樣可以將資料存取邏輯與業務邏輯分離,提高程式碼的可維護性和可測試性。
領域建模
本章節將探討如何以程式碼建模業務流程,並與測試驅動開發(TDD)高度相容。我們將討論領域建模的重要性,並介紹一些關鍵的建模模式,包括實體(Entity)、值物件(Value Object)和領域服務(Domain Service)。
什麼是領域模型?
在前言中,我們使用「業務邏輯層」來描述三層架構中的核心層。本文將使用「領域模型」這個術語,它更好地捕捉了我們的預期含義(有關DDD的更多資訊,請參閱下一個側邊欄)。
領域是指你試圖解決的問題。作者目前在一家線上傢具零售商工作。根據所討論的系統,領域可能涉及採購、產品設計或物流和配送。大多數程式設計師每天都在嘗試改善或自動化業務流程;領域是這些流程所支援的活動集合。
模型是捕捉某個過程或現象中有用屬性的地圖。人類非常擅長在腦海中建立事物的模型。例如,當有人朝你扔球時,你幾乎無意識地就能預測它的運動軌跡,因為你有一個關於物體在空間中運動的模型。你的模型並不完美,人類對於接近光速或在真空中的物體行為幾乎沒有直覺,因為我們的模型從未被設計來涵蓋這些情況。這並不意味著模型是錯誤的,而是意味著某些預測超出了其領域範圍。
領域模型是業務所有者對其業務的心理地圖。所有業務人員都有這些心理地圖——這是人類思考複雜過程的方式。
當他們瀏覽這些地圖時,你可以從他們使用的業務術語中看出來。術語在協作處理複雜系統的人群中自然產生。
想像一下,你和你的朋友、家人突然被運送到離地球很遠的某個外星飛船上,需要從頭開始弄清楚如何傳回家鄉。在最初的幾天裡,你可能會隨機按按鈕,但很快你就會學會哪些按鈕控制什麼,這樣你就可以給對方下達指令。「按下閃爍裝置附近的紅色按鈕,然後拉下雷達裝置旁邊的大槓桿,」你可能會這樣說。
幾周後,你們會變得更加精確,因為你們採用了描述飛船功能的詞彙:「增加三號貨艙的氧氣含量」或「開啟小型推進器」。幾個月後,你們就會採用描述整個複雜過程的語言:「開始著陸程式」或「準備進入曲速狀態」。這個過程會很自然地發生,無需正式努力建立分享詞彙表。
內容解密:
- 領域模型的概念源自領域驅動設計(DDD),強調軟體應當提供對問題的有用模型。
- 正確的模型能夠為業務帶來價值並實作新的可能性。
- DDD涵蓋了許多架構模式,如實體、聚合、值物件和倉儲等。
為什麼領域建模很重要?
領域建模至關重要,因為它能夠讓開發者深入理解業務流程和規則。透過建立一個準確反映業務領域的模型,軟體系統能夠更好地支援業務運作,並隨著業務需求的變化而靈活調整。
圖 1-1:領域模型的簡單視覺化表示
補充說明:
此圖示展示了領域模型的核心組成部分,包括實體、值物件和領域服務之間的關係。
內容解密:
- 圖表展示了領域模型的基礎架構。
- 實體代表具有唯一識別符的物件。
- 值物件是不可變的,用於描述某些屬性。
- 領域服務封裝了業務邏輯,用於協調實體和值物件之間的互動作用。
庫存分配機制的創新與領域建模
業務場景與創新點
傳統的庫存管理方式僅考慮倉函式庫中的實物庫存,當倉庫存貨不足時,產品會被標記為「缺貨」。本案例中的企業決定採用一種新的庫存分配機制,將運輸中的貨物視為可用庫存的一部分,以減少缺貨情況並降低倉庫存貨成本。
領域語言的探索
透過與業務專家的深入討論,建立了領域模型的初步詞彙表和規則。為確保模型的準確性,使用具體的業務案例來說明每條規則,並採用業務專家熟悉的術語來表達這些規則。
庫存分配的關鍵規則
- 產品識別:使用SKU(庫存量單位)來識別產品。
- 訂單結構:客戶訂單包含多個訂單明細,每個明細包含SKU和數量。
- 批次管理:採購部門訂購小批次的庫存,每個批次有唯一的參考編號、SKU和數量。
- 分配邏輯:
- 將訂單明細分配給特定的批次,並從該批次的可用數量中扣除相應數量。
- 不能將訂單明細分配給可用數量不足的批次。
- 不能重複將同一訂單明細分配給同一批次。
- 優先分配順序:優先分配給倉函式庫庫存,其次是運輸中的批次,並按照預計到達時間(ETA)的先後順序進行分配。
領域模型的單元測試
透過編寫單元測試來驗證領域模型的正確性。測試案例涵蓋了分配邏輯的主要規則,例如:
def test_allocating_to_a_batch_reduces_the_available_quantity():
batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
line = OrderLine('order-ref', "SMALL-TABLE", 2)
batch.allocate(line)
assert batch.available_quantity == 18
初步的領域模型實作
定義了OrderLine和Batch兩個類別,分別代表訂單明細和批次。
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self.available_quantity = qty
def allocate(self, line: OrderLine):
self.available_quantity -= line.qty
程式碼解析:
OrderLine類別:使用@dataclass(frozen=True)裝飾器定義了一個不可變的資料類別,包含orderid、sku和qty三個屬性,分別代表訂單ID、產品SKU和數量。Batch類別:定義了批次的基本屬性,包括參考編號、SKU、預計到達時間(ETA)和可用數量。allocate方法用於將訂單明細分配給批次,並更新批次的可用數量。- 型別提示:使用型別提示(如
str、int、Optional[date])來提高程式碼的可讀性和維護性。
未來改進方向
目前的實作相對簡單,未來可以考慮增加更多的業務規則和邏輯,例如處理多個批次之間的分配優先順序、批次合併等情況,以進一步完善領域模型。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 領域驅動設計 DDD 架構
package "應用層" {
component [Service Layer] as service
component [Unit of Work] as uow
}
package "領域層" {
package "領域模型" {
component [Entity 實體] as entity
component [Value Object 值物件] as vo
component [Domain Service] as ds
}
package "聚合" {
component [Aggregate Root] as ar
component [聚合邊界] as boundary
}
}
package "基礎設施層" {
component [Repository 倉儲] as repo
component [ORM 映射] as orm
component [資料庫] as db
}
package "測試驅動開發" {
component [單元測試] as unit_test
component [整合測試] as int_test
}
service --> uow : 交易管理
service --> ds : 業務邏輯
entity --> vo : 組合使用
entity --> ar : 聚合根
ar --> boundary : 一致性邊界
repo --> orm : 資料存取
orm --> db : 持久化
unit_test --> entity : 測試領域邏輯
int_test --> repo : 測試資料存取
note right of entity
Entity 特性:
- 具有唯一識別
- 生命週期追蹤
end note
note right of vo
Value Object 特性:
- 無識別,以值定義
- 不可變 (Immutable)
end note
note right of repo
Repository 模式:
- 封裝資料存取
- 隔離領域與基礎設施
end note
@enduml
補充說明:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 補充:庫存分配領域模型
package "庫存分配領域" {
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
此圖示展示了客戶訂單、訂單明細和批次之間的關係,以及分配邏輯的基本流程。
圖示解析:
- 客戶訂單包含多個訂單明細。
- 訂單明細被分配給特定的批次。
- 分配後,批次的可用數量會被更新。
- 系統會檢查批次的可用數量是否足夠,若不足則不允許分配。
領域模型驅動設計:批次管理系統實作
在批次管理系統的開發過程中,我們採用領域驅動設計(DDD)原則來建立一個強壯且可維護的模型。以下將介紹如何實作批次(Batch)與訂單明細(OrderLine)之間的分配邏輯。
測試驅動開發
首先,我們透過測試驅動開發(TDD)來確保我們的模型行為正確。我們定義了多個測試案例來驗證 can_allocate 方法的正確性。
# test_batches.py
def make_batch_and_line(sku, batch_qty, line_qty):
return (
Batch("batch-001", sku, batch_qty, eta=date.today()),
OrderLine("order-123", sku, line_qty)
)
def test_can_allocate_if_available_greater_than_required():
large_batch, small_line = make_batch_and_line("ELEGANT-LAMP", 20, 2)
assert large_batch.can_allocate(small_line)
def test_cannot_allocate_if_available_smaller_than_required():
small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20)
assert not small_batch.can_allocate(large_line)
def test_can_allocate_if_available_equal_to_required():
batch, line = make_batch_and_line("ELEGANT-LAMP", 2, 2)
assert batch.can_allocate(line)
def test_cannot_allocate_if_skus_do_not_match():
batch = Batch("batch-001", "UNCOMFORTABLE-CHAIR", 100, eta=None)
different_sku_line = OrderLine("order-123", "EXPENSIVE-TOASTER", 10)
assert not batch.can_allocate(different_sku_line)
內容解密:
make_batch_and_line函式用於建立Batch和OrderLine物件,簡化測試資料的建立。- 多個測試函式驗證了
can_allocate方法在不同場景下的行為,包括庫存足夠、不足、相等以及 SKU 不匹配的情況。 - 這些測試案例確保了
can_allocate方法的正確性。
領域模型實作
接下來,我們實作了 Batch 類別的 can_allocate 方法。
# model.py
class Batch:
def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
self.reference = ref
self.sku = sku
self.eta = eta
self._purchased_quantity = qty
self._allocations = set() # type: Set[OrderLine]
def can_allocate(self, line: OrderLine) -> bool:
return self.sku == line.sku and self.available_quantity >= line.qty
@property
def available_quantity(self) -> int:
return self._purchased_quantity - self.allocated_quantity
@property
def allocated_quantity(self) -> int:
return sum(line.qty for line in self._allocations)
內容解密:
can_allocate方法檢查 SKU 是否匹配以及可用數量是否足夠。available_quantity屬性計算可用數量,即採購數量減去已分配數量。allocated_quantity屬性計算已分配的總數量。
UML 模型
我們的領域模型可以用 UML 圖表示,如下所示:
此圖示呈現了 Batch 和 OrderLine 之間的關係。
更多型別提示
為了提高程式碼的可讀性和安全性,我們可以使用 typing.NewType 來建立更具體的型別。
# model.py
from typing import NewType
Quantity = NewType("Quantity", int)
Sku = NewType("Sku", str)
Reference = NewType("Reference", str)
class Batch:
def __init__(self, ref: Reference, sku: Sku, qty: Quantity):
self.sku = sku
self.reference = ref
self._purchased_quantity = qty
內容解密:
- 使用
NewType建立了Quantity、Sku和Reference等新型別。 - 這些新型別可以用於提高函式引數和屬性的型別安全性。
領域驅動設計中的值物件與實體
在領域驅動設計(Domain-Driven Design, DDD)中,我們經常需要決定如何表示業務概念。有些概念具有唯一的身份,而有些則僅由其資料定義。在本篇文章中,我們將探討值物件(Value Object)和實體(Entity)的區別,以及如何在 Python 中實作它們。
值物件
值物件是由其資料唯一標識的領域物件。它們通常是不可變的,並且具有值相等性(value equality)。也就是說,如果兩個值物件具有相同的資料,則它們被視為相等。
OrderLine 作為值物件
from dataclasses import dataclass
@dataclass(frozen=True)
class OrderLine:
orderid: str
sku: str
qty: int
值物件的例子
from dataclasses import dataclass
from typing import NamedTuple
@dataclass(frozen=True)
class Name:
first_name: str
surname: str
class Money(NamedTuple):
currency: str
value: int
Line = NamedTuple('Line', ['sku', 'qty'])
def test_equality():
assert Money('gbp', 10) == Money('gbp', 10)
assert Name('Harry', 'Percival') != Name('Bob', 'Gregory')
assert Line('RED-CHAIR', 5) == Line('RED-CHAIR', 5)
值物件上的數學運算
fiver = Money('gbp', 5)
tenner = Money('gbp', 10)
def can_add_money_values_for_the_same_currency():
assert fiver + fiver == tenner
def can_subtract_money_values():
assert tenner - fiver == fiver
def adding_different_currencies_fails():
with pytest.raises(ValueError):
Money('usd', 10) + Money('gbp', 10)
def can_multiply_money_by_a_number():
assert fiver * 5 == Money('gbp', 25)
def multiplying_two_money_values_is_an_error():
with pytest.raises(TypeError):
tenner * fiver
實體
實體具有長期的身份,並且可以更改其值。即使其值發生變化,它仍然是相同的實體。
Batch 作為實體
class Batch:
def __init__(self, reference, sku, qty, eta):
self.reference = reference
self.sku = sku
self.qty = qty
self.eta = eta
def __eq__(self, other):
if not isinstance(other, Batch):
return False
return other.reference == self.reference
def __hash__(self):
return hash(self.reference)
值物件與實體的區別
值物件由其資料定義,而實體具有獨立於其值的身份。
Person 作為實體的例子
class Person:
def __init__(self, name: Name):
self.name = name
def test_barry_is_harry():
harry = Person(Name("Harry", "Percival"))
barry = harry
barry.name = Name("Barry", "Percival")
assert harry is barry and barry is harry
領域服務函式
有時,我們需要執行一些不自然地屬於某個實體或值物件的操作。在這種情況下,我們可以使用領域服務函式。
allocate 函式
def allocate(line: OrderLine, batches: List[Batch]):
# implementation details
pass
def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
shipment_batch = Batch("shipment-batch", "RETRO-CLOCK", 100, eta=tomorrow)
line = OrderLine("oref", "RETRO-CLOCK", 10)
allocate(line, [in_stock_batch, shipment_batch])
assert in_stock_batch.available_quantity == 90
assert shipment_batch.available_quantity == 100
在本文中,我們探討了值物件和實體在領域驅動設計中的作用,以及如何在 Python 中實作它們。我們還討論了領域服務函式的使用場景。透過這些概念,我們可以更好地建模業務領域,並實作更健壯、更易於維護的軟體系統。
詳細解說
- 值物件:主要由其資料定義,具有不可變性。它們通常用於表示一些簡單的概念,如金額、姓名等。
- 實體:具有獨立於其值的長期身份。它們可以更改其屬性,但仍然保持相同的身份。
- 領域服務函式:用於處理那些不自然地屬於某個特定實體或值物件的操作。它們提供了一種靈活的方式來組織程式碼,使得業務邏輯更加清晰。