iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Software Development

消除你程式碼的臭味系列 第 11

Day 11- 函式的副作用:把計算與 I/O 分離

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 11- 函式的副作用:把計算與 I/O 分離

一個函式應該只做一件事,並且做好它。

如果你的函式既要處理計算,又要跟外部世界(螢幕、檔案、網路)打交道,那它就是散發臭味了。

讓計算歸計算,I/O 歸邊界。

什麼是副作用 Side Effect?

「副作用」指的是函式在執行過程中,做了除了「回傳一個值」以外的任何事。

像一個廚師在料理過程中,自己跑出廚房的行為。
在程式碼中,「副作用」就是函式跨越了自身的邊界,與它控制範圍外的世界進行了互動

一個好的函式, 應該讓廚師專注在自己的廚房裡。 他的任務是「料理」。
你把所有食材(參數)一次性給他,他就在廚房裡專心處理這些食材,最後端出一道菜(回傳值)。

他不管食材是從哪裡來的,只專心做菜。這個過程是可預測的:同樣的食材,一定會做出同樣的菜。

https://ithelp.ithome.com.tw/upload/images/20250913/20124462NaC8dabXuN.png

只要你的函式有這些行為,它就變得不可預測
同樣的輸入,可能因為網路斷線或檔案權限問題而產生完全不同的結果,這就是發臭的開始。

分層指引管理副作用

可以將系統劃分為不同的層次,建立清晰的資料流(Data Pipeline),來管理副作用:

  • 核心層 (Core):主要邏輯的資料轉換與運算,只包含純函式 (Pure Functions),完全不觸碰外部。

  • 邊界層 (Boundary):處理所有 I/O 操作與外部系統的整合。這一層是副作用的集中地,也統一進行監控與錯誤處理。

  • 組裝層 (Composition):負責串連核心層與邊界層,將依賴注入,協調完整的業務流程。

經典案例:匯出一份邏輯報告

// 🔴 髒臭味:商業邏輯、檔案讀取、檔案寫入、資料格式化,全綁在一起。
function generateVipDiscountReport(inputFile, outputFile) {
  try {
    // --- 副作用:讀取檔案 (I/O) ---
    const data = fs.readFileSync(inputFile, 'utf8');

    const users = data.split('\n').map(line => {
      const [name, level, amount] = line.split(',');
      return { name, level, amount: Number(amount) };
    });

    let reportLines = [];
    for (const user of users) {
      // --- 主要商業邏輯 ---
      if (user.level === 'VIP' && user.amount > 1000) {
        const discount = user.amount * 0.1; // 10% 折扣
        // --- 資料格式化邏輯 ---
        reportLines.push(`${user.name},${discount}`);
      }
    }

    // --- 副作用:寫入檔案 (I/O) ---
    fs.writeFileSync(outputFile, reportLines.join('\n'));
    console.log('Report generated.');

  } catch (error) {
    console.error('Error generating report:', error);
  }
}

這段程式碼無法測試、無法重用、職責混亂。
如果需求改成從資料庫讀取,你就得重寫。
如果折扣規則改變,還要來碰一個處理檔案的函式。

https://ithelp.ithome.com.tw/upload/images/20250913/20124462CzqKrqLlyS.png

分離關注點計算與 I/O

// 🟢 好品味:一個函式只做一件事
const fs = require('fs');

// 1. 核心層 主要商業邏輯 (純函式)
//    - 它不認識檔案系統。它只懂資料。這才是你該測試的地方。
function processUserData(users) {
  return users
    .filter(u => u.level === 'VIP' && u.amount > 1000)
    .map(u => ({
      name: u.name,
      discount: u.amount * 0.1,
    }));
}

// 2. 邊界層工具 (處理 I/O)
//    - 這些函式很笨,只負責跟外部世界溝通。
function readUsersFromFile(filePath) {
  const data = fs.readFileSync(filePath, 'utf8');
  return data.split('\n').map(line => {
    const [name, level, amount] = line.split(',');
    return { name, level, amount: Number(amount) };
  });
}

function writeReportToFile(filePath, processedData) {
  const reportLines = processedData.map(d => `${d.name},${d.discount}`);
  fs.writeFileSync(filePath, reportLines.join('\n'));
}

// 3. 組裝層 膠水函式 (Orchestrator)
//    - 協調主要與邊界,完成整個流程。
function generateReport(inputFile, outputFile) {
  try {
    const users = readUsersFromFile(inputFile);
    const processedData = processUserData(users);
    writeReportToFile(outputFile, processedData);
    console.log('Report generated successfully.');
  } catch (error) {
    console.error('Error in report generation process:', error);
  }
}
  • 測試變得更簡單:你可以用一百種假資料陣列去測試 processUserData,確保所有商業邏輯都正確無誤。整個過程不需要模擬 (mock) 任何東西。

  • 可以重複利用:明天,資料源變成資料庫?很好,你只需要寫一個 readUsersFromDatabase() 來替換 readUsersFromFile()。主要商業邏輯 processUserData 一行都不用改。

  • 除錯更直接:報告裡的數字錯了?問題 100% 在 processUserData。報告檔案沒產生?問題 100% 在 writeReportToFile。問題被精準地隔離了。

https://ithelp.ithome.com.tw/upload/images/20250913/20124462NtsiLOuCFr.png

除臭步驟

別想太多,就兩步:

  1. 把所有計算邏輯、商業規則抓出來,放進一個新的、純粹的函式裡。

  2. 結束了。

所有外部世界的東西(資料庫、設定檔、時間)都應該作為參數傳進去,而不是在函式內部去讀取。

今日重點

真正的問題只有一個:「這個函式是不是只做一件事?」。
如果不是,就去修改它。


上一篇
Day 10- 介面最小化:只暴露必要的東西
下一篇
Day 12- 拒絕複製貼上:抽出共用邏輯
系列文
消除你程式碼的臭味12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言