iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

文科生的軟體工程啟蒙:用一個代購 App,看懂 30 個系統設計觀念系列 第 21 篇

Day 21:可測試性架構(Testability)——純函式(Pure Functions)與無副作用(Side Effects)的價值

  • 分享至 

  • xImage
  •  

昨天談重構時提到一個關鍵前提:每改一小步,就要重新跑測試,確認外部行為沒有改變。但這句話藏著一個沒講清楚的問題——程式碼要「容易被測試」,本身也是一種需要刻意設計出來的特質。今天要談的純函式與副作用,就是決定一段程式碼好不好測試的核心因素。

一、什麼是副作用(Side Effect)

定義:副作用是指一個函式在回傳結果之外,還對「外部世界」做了某些改動——例如寫入資料庫、修改外部變數、發送網路請求、印出 log。副作用不是壞事,系統終究需要真的把資料存進資料庫、把通知送出去,但副作用會讓函式的行為跟「外部環境的狀態」綁在一起。

二、什麼是純函式(Pure Function)

定義:純函式必須滿足兩個條件:

  1. 相同輸入,永遠得到相同輸出:不受外部變數、時間、隨機數等因素影響。
  2. 不修改任何外部狀態:不改外部變數、不寫資料庫、不觸發任何看得見的副作用。

三、為什麼純函式容易測試

測試一個函式,本質上是「給定輸入,檢查輸出是否符合預期」。如果函式是純函式,測試只需要準備輸入、呼叫函式、比對輸出,完全不需要先架設資料庫、不需要清理測試資料、也不用擔心測試執行的順序會不會互相干擾。反過來,如果函式內部直接呼叫資料庫或外部 API,測試就必須想辦法「模擬」這些外部依賴,測試的複雜度跟著一起上升——這也是為什麼好的架構,會盡量把「計算邏輯」跟「有副作用的操作」分開存放。

四、代購 App 案例

延續 Day 18 的 OrderService。訂單的金額計算是一段純邏輯,不需要碰資料庫,把它抽成一個獨立的純函式,就能直接測試:

// pure/calculateTotal.js —— 純函式:相同輸入永遠得到相同輸出,不碰資料庫、不發通知
export function calculateTotal(price, quantity, shippingFee) {
  return price * quantity + shippingFee;
}
// 測試這個函式完全不需要資料庫,只要給輸入比對輸出
// calculateTotal(100, 2, 30) 永遠等於 230

// services/orderService.js —— 有副作用的部分集中在這裡,跟純邏輯分開
async function createOrder(input) {
  const total = calculateTotal(input.price, input.quantity, input.shippingFee); // 呼叫純函式
  const order = await orderRepository.save({ ...input, total }); // 副作用:寫入資料庫
  await notifier.notify(order.buyerId, `訂單總額 ${total} 元`); // 副作用:發送通知
  return order;
}

把金額計算抽成純函式之後,calculateTotal 本身幾乎不需要特別的測試技巧;而真正需要處理「如何模擬副作用」的,只剩下 createOrder 裡那兩行跟資料庫、通知服務打交道的程式碼——測試的複雜度被集中管理,而不是散落在每一個函式裡。

五、結論

純函式之所以重要,不是因為它比較「優雅」,而是因為它把「驗證邏輯是否正確」這件事,跟「外部世界的狀態」徹底切開。能分離出來的邏輯,就儘量寫成純函式;真正需要碰資料庫、網路的部分,則集中、縮小範圍地存放,這正是讓 Day 20 講的「重構後重新跑測試」這件事,能夠又快又可靠地執行下去的前提。


上一篇
Day 20:重構策略(Refactoring)——如何在不改變外部行為的前提下安全調整內部結構
系列文
文科生的軟體工程啟蒙:用一個代購 App,看懂 30 個系統設計觀念 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言