函數式程式設計(FP)往往讓習慣指令式程式設計的人困惑:
「變數(Variables)不會變(Do not vary)。」
在純粹的 FP 語言中(如 Clojure、Lisp),一旦變數被初始化,就永遠不會被修改。
這聽來抽象,但對軟體架構師而言,這解決一個至關重要的問題:併發(Concurrency)。
一、不可變性與架構 (Immutability and Architecture)#
為什麼架構師要在乎變數是否可變?
因為所有併發問題(死鎖、競爭條件、並行更新問題)都源於可變變數(Mutable Variables)。
- 因果關係: 如果沒有變數會被修改,就永遠不會有兩個執行緒試圖同時修改同個變數
- 結論: 如果系統完全不可變,那它就是完全執行緒安全(Thread-safe),不需要任何鎖(Locks)
當然,現實軟體系統常需要狀態改變(如更新使用者餘額),因此無法做到 100% 的不可變。
但我們可以透過架構設計來控制它。
二、方案 A:可變性的隔離 (Segregation of Mutability)#
既然無法完全消除可變性,架構師的策略就是隔離。
我們將應用程式拆分為兩大區塊:
- 不可變元件 (Immutable Components):
- 以純函數(Pure Functions)執行任務
- 負責多數邏輯運算
- 不改變任何狀態
- 可變元件 (Mutable Components):
- 與不可變元件通訊,並管理狀態變更
- 用交易式記憶體 (Transactional Memory) 保護變數,確保原子性(Atomicity)
架構目標: 盡可能將程式碼推往「不可變元件」,讓「可變元件」越小越好。

Figure 6.1: Mutating state and transactional memory
三、方案 B:事件溯源 (Event Sourcing)#
如果不更新變數,還有什麼方法可以管理狀態?
如果擁有無限的儲存空間和運算能力,可以採用事件溯源。
- 概念: 不存結果(State),儲存過程(Transactions)
- 例子:
- 傳統做法: 帳戶餘額為
100。存入50時,將變數更新為150(這需要鎖) - 事件溯源: 不存餘額,只存一連串紀錄:
[開戶, 存入100, 存入50]。
需要知道餘額時,從頭計算一遍
- 傳統做法: 帳戶餘額為
- 優點:
- 因只進行「新增(Append)」操作,不進行「更新(Update)」或「刪除(Delete)」
- CRUD 變成了 CR
- 既然沒有更新,就不會有併發更新的問題
函數式程式設計教導我們的架構課是:資料變更(Assignment)是危險的。
身為架構師,我們應限制並嚴格管理資料何時、以及如何被修改。