職涯生涯 做了學術研究很久 真的寫不出來
偶然協助客戶開發一套有點複雜的小電商 在我表達我的理想之後 claude opus 好像懂我方向
幫我寫出來了 我感覺 就像是終於觸摸到了上帝
我沒辦法清楚表達 這套 paradigm ... 但是比什麼都優雅
PHP 的 readonly class + private constructor + 回傳 self,剛好能把這個概念表達得非常自然
最近在設計購物車時,我發現一件很有趣的事:
當 DDD 的 Domain Object 採用 immutable 設計後,原本看似典型的 OOP,會自然呈現出 Functional Programming 的樣子。
例如:
$newCart = $cart->removeItem($sku);
表面上這是一個物件方法,但從函數的角度來看,它其實是:
removeItem : CartInput → CartInput
也就是:
舊狀態 → 操作 → 新狀態
物件不再被原地修改,而是每次操作都產生一個新的完整狀態。
傳統 mutable OOP 通常是:
$cart->removeItem($sku);
同一個物件的內部狀態被改變。
這會帶來一些問題:
Immutable OOP 則是:
$nextCart = $cart->removeItem($sku);
原本的 $cart 完全不變,removeItem() 只是將它映射成另一個 CartInput。
Cart₀ → Cart₁ → Cart₂ → Cart₃
每一個版本都是獨立、完整且合法的值。
DDD 主要回答:
FP 主要回答:
兩者結合後,可以簡化成一句話:
DDD 定義 domain 的語意,FP 定義 domain 狀態如何演進。
以購物車為例:
final readonly class CartInput
{
public function removeItem(string $sku): self
{
$items = $this->items;
unset($items[$sku]);
return new self(
items: $items,
coupon: $this->coupon,
addOns: $this->addOns,
);
}
}
如果 constructor 負責維護 invariant:
private function __construct(
array $items,
?string $coupon,
array $addOns,
) {
if ($items === []) {
$addOns = [];
}
$this->items = $items;
$this->coupon = $coupon;
$this->addOns = $addOns;
}
那麼每次操作的實際流程就是:
舊的合法狀態
→ 建立候選狀態
→ 套用 invariant
→ 新的合法狀態
因此系統不再依賴呼叫者記得:
$cart->removeItem($sku);
$cart->cleanupAddOns();
$cart->validate();
所有路徑最後都必須經過 constructor,所以不合法狀態根本無法存在。
這比「出錯後再修復」更可靠。
以下兩種寫法,本質非常接近:
$nextCart = $cart->removeItem($sku);
$nextCart = removeItem($cart, $sku);
第一種只是把函數的第一個參數移到箭頭左邊。
因此 immutable object 的 method 不再像傳統 command:
叫某個物件修改自己
而更像 transformation:
把某個值轉換成另一個值
這就是為什麼 immutable OOP 會讓人感覺非常接近 FP。
如果 removeItem() 明顯是購物車本身的行為,放進 CartInput 最自然:
$cart->removeItem($sku);
若改用 Domain Service:
$cartService->removeItem($cart, $sku);
很容易逐漸變成:
CartInput = 純資料
CartService = 所有行為的大雜燴
但 Domain Service 並不是沒有用途。
當一個操作:
此時 Domain Service 仍然合理。
關鍵不是排斥 Service,而是辨認:
這是某個 domain state 自己的 transformation,還是真正跨物件的 domain operation?
整體可以分成幾層:
CartInput
── addItem / removeItem / withCoupon ──▶ CartInput
這一層描述客戶意圖如何演進。
CartInput
── PricingPipeline ──▶ CartState
這一層描述系統如何解讀客戶意圖,產生折扣、贈品、加價購與最終金額。
CartState
── Application Layer ──▶ Database / Email / Payment
最後才執行資料庫、寄信、付款等副作用。
因此可以得到一個非常清晰的系統:
Domain types
+ immutable values
+ pure state transitions
+ explicit side effects
這不是 OOP 被 FP 取代,也不是 DDD 變成 FP。
更精確地說:
當 OOP 放棄原地 mutation,物件方法就會顯露出函數轉換的本質。
DDD 提供:
FP 提供:
最後形成的就是:
語意上是 DDD Domain Object,運作方式上是 Functional State Transformation。
這種寫法既保留 OOP 的可讀性,又得到 FP 的穩定性,是 Functional Core 與 Domain Modeling 非常自然的交會點。