在講怎麼組裝 Effect 建構程式之前,本文會先比較兩種建立 pipeline(指一系列的步驟,每個步驟都會接收前一個步驟的輸出,並回傳新的輸出) 的方式。並解釋為何在 Effect 的設計裡,Function pipeline 更為契合:
例如,在價格計算的情境中,我們會先套用折扣,接著加上稅金,最後將結果取整數。
class PriceChain {
constructor(private currentAmount: number) {}
applyDiscount(percent: number) {
this.currentAmount = Math.round(this.currentAmount * (1 - percent))
return this
}
addTax(rate: number) {
this.currentAmount = Math.round(this.currentAmount * (1 + rate))
return this
}
roundToDollar() {
this.currentAmount = Math.round(this.currentAmount)
return this
}
total() {
return this.currentAmount
}
}
const total = new PriceChain(1000)
.applyDiscount(0.1)
.addTax(0.05)
.roundToDollar()
.total()
console.log(total) // 945
因為 method chaining 會把狀態綁在同一個實例上,呼叫順序會影響結果。
class MutablePriceChain {
constructor(private amount: number) {}
applyDiscount(percent: number) {
this.amount = Math.round(this.amount * (1 - percent))
return this
}
addTax(rate: number) {
this.amount = Math.round(this.amount * (1 + rate))
return this
}
total() {
return this.amount
}
}
const shared = new MutablePriceChain(1000)
function flowA() {
return shared.applyDiscount(0.1).total() // shared 變為 900(狀態被改動)
}
function flowB() {
return shared.addTax(0.05).total() // 對 900 加稅,而非預期的 1000
}
const a = flowA()
const b = flowB()
console.log(a) // 900
console.log(b) // 945(非預期,原本可能期望 1050)
不過這個問題還是有解決方法,就是改成不可變的設計,讓每步驟都產生新的實例,避免共享狀態污染。
class ImmutablePriceChain {
constructor(private readonly amount: number) {}
applyDiscount(percent: number) {
return new ImmutablePriceChain(Math.round(this.amount * (1 - percent)))
}
addTax(rate: number) {
return new ImmutablePriceChain(Math.round(this.amount * (1 + rate)))
}
total() {
return this.amount
}
}
const base = new ImmutablePriceChain(1000)
function flowA() {
return base.applyDiscount(0.1).total() // 900(base 本身不變)
}
function flowB() {
return base.addTax(0.05).total() // 1050(如預期,未受 flowA 影響)
}
const a = flowA()
const b = flowB()
console.log(a) // 900
console.log(b) // 1050
但使用 method chaining 仍把行為綁在 class 內。所以擴充與重用仍受限於 class 的邊界。
這種寫法也更貼近業務語意:方法名稱通常就是業務動詞(如 applyDiscount
、addTax
、roundToDollar
),同一個實例對應同一個業務實體(如訂單、購物車、查詢),讀起來像自然語言的「對這個東西做事」。同時,不變條件與驗證能封裝在 method 內,呼叫端就像使用領域語言(DSL);再加上 IDE 的自動完成可列出可用動作,探索成本更低。
適合流程固定、語義明確的業務邏輯(如 ORM Query Builder:常見步驟固定,像選表/欄位 → 篩選 → 連接 → 排序 → 分頁,且一路操作同一個查詢實例)。
函數式的作法是將每個步驟設計為「接收輸入、回傳輸出」的純函數。假設我們已有步驟函數(如 applyDiscount
、addTax
、roundToDollar
),可以這樣手動串接:
function applyDiscount(percent: number): (amount: number) => number {
function apply(amount: number): number {
return Math.round(amount * (1 - percent))
}
return apply
}
function addTax(rate: number): (amount: number) => number {
function add(amount: number): number {
return Math.round(amount * (1 + rate))
}
return add
}
function roundToDollar(): (amount: number) => number {
function round(amount: number): number {
return Math.round(amount)
}
return round
}
const s1 = applyDiscount(0.1)(1000)
const s2 = addTax(0.05)(s1)
const totalManual = roundToDollar()(s2)
console.log(totalManual) // 945
問題:
s1
、s2
),樣板碼偏多。function pipe<T>(input: T, ...steps: Array<(value: any) => any>) {
return steps.reduce((acc, step) => step(acc), input)
}
const total = pipe(1000, applyDiscount(0.1), addTax(0.05), roundToDollar())
console.log(total) // 945
pipe
裡面的函數清單,不用改一堆中間變數,也比較不會傳錯值。pipe
只是把累積值 acc
逐步傳遞給下一個函式。Effect 採用函數式組合(例如 pipe
)塑造資料流,強調長期可維護性(可組合、可重用、易抽象)。對於顯式傳遞依賴的不便,提供 Context
與 Layer
進行依賴管理與組裝,並透過 Effect.gen
以更直觀的順序式風格組合 Effect。下一篇將示範如何用 Effect 建構資料流。