職涯生涯 做了學術研究很久 真的寫不出來

偶然協助客戶開發一套有點複雜的小電商 在我表達我的理想之後 claude opus 好像懂我方向

幫我寫出來了 我感覺 就像是終於觸摸到了上帝

我沒辦法清楚表達 這套 paradigm ... 但是比什麼都優雅


PHP 的 readonly class + private constructor + 回傳 self,剛好能把這個概念表達得非常自然


Immutable DDD:當 OOP 自然走向 FP

最近在設計購物車時,我發現一件很有趣的事:

當 DDD 的 Domain Object 採用 immutable 設計後,原本看似典型的 OOP,會自然呈現出 Functional Programming 的樣子。

例如:

$newCart = $cart->removeItem($sku);

表面上這是一個物件方法,但從函數的角度來看,它其實是:

removeItem : CartInput → CartInput

也就是:

舊狀態 → 操作 → 新狀態

物件不再被原地修改,而是每次操作都產生一個新的完整狀態。


Mutable OOP 與 Immutable OOP

傳統 mutable OOP 通常是:

$cart->removeItem($sku);

同一個物件的內部狀態被改變。

這會帶來一些問題:

  • 不容易知道物件在哪裡被修改
  • 操作順序會影響結果
  • 可能出現暫時不合法的中間狀態
  • 多個地方持有相同 reference 時更難推理
  • 呼叫者可能忘記執行 cleanup 或 validation

Immutable OOP 則是:

$nextCart = $cart->removeItem($sku);

原本的 $cart 完全不變,removeItem() 只是將它映射成另一個 CartInput

Cart₀ → Cart₁ → Cart₂ → Cart₃

每一個版本都是獨立、完整且合法的值。


DDD 與 FP 各自負責什麼

DDD 主要回答:

  • 這個商業概念叫什麼?
  • 哪些行為屬於這個物件?
  • 哪些狀態是合法的?
  • invariant 應該由誰維護?
  • domain boundary 在哪裡?

FP 主要回答:

  • 狀態如何轉換?
  • 能否避免原地修改?
  • 能否讓相同輸入得到相同輸出?
  • 能否把副作用隔離在系統邊界?
  • 能否將小型 transformation 組合成完整流程?

兩者結合後,可以簡化成一句話:

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,所以不合法狀態根本無法存在。

這比「出錯後再修復」更可靠。


Object Method 本質上也可以是 Function

以下兩種寫法,本質非常接近:

$nextCart = $cart->removeItem($sku);
$nextCart = removeItem($cart, $sku);

第一種只是把函數的第一個參數移到箭頭左邊。

因此 immutable object 的 method 不再像傳統 command:

叫某個物件修改自己

而更像 transformation:

把某個值轉換成另一個值

這就是為什麼 immutable OOP 會讓人感覺非常接近 FP。


Domain Service 不需要承擔所有行為

如果 removeItem() 明顯是購物車本身的行為,放進 CartInput 最自然:

$cart->removeItem($sku);

若改用 Domain Service:

$cartService->removeItem($cart, $sku);

很容易逐漸變成:

CartInput = 純資料
CartService = 所有行為的大雜燴

但 Domain Service 並不是沒有用途。

當一個操作:

  • 涉及多個 domain object
  • 需要外部 policy
  • 不自然屬於任何單一物件
  • 需要跨 aggregate 協作

此時 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 提供:

  • 商業語意
  • 封裝
  • invariant
  • domain boundary

FP 提供:

  • immutable state
  • pure transformation
  • composition
  • 可推理性

最後形成的就是:

語意上是 DDD Domain Object,運作方式上是 Functional State Transformation。

這種寫法既保留 OOP 的可讀性,又得到 FP 的穩定性,是 Functional Core 與 Domain Modeling 非常自然的交會點。


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 小網站開發服務(功能明確、規模不大的需求)
https://job.turn.tw/small-website-services

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA

共有 0 則留言


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 小網站開發服務(功能明確、規模不大的需求)
https://job.turn.tw/small-website-services

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA
🏆 本月排行榜
🥇
站長阿川
📝8   💬2   ❤️4
237
🥈
我愛JS
📝2   💬6   ❤️3
110
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次