iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Software Development

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

Day 25- 資源管理:打開的東西就要關掉

  • 分享至 

  • xImage
  •  

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

消除你程式碼的臭味 Day 25- 資源管理:打開的東西就要關掉

打開的東西,就要關掉。這次我講個故事。

假設去圖書館借了全世界只有一本的書。
圖書館員(就是作業系統)告訴你:「你看完之後,必須親手把書還回來,否則下一個人就永遠借不到了。」

這本書,就是一個「資源」——一個檔案、一個網路連線、一個資料庫交易。

系統裡的這些資源數量是有限的。
如果你借了不還,系統會造成資源洩漏 (Resource Leak) ,洩漏幾個沒感覺,洩漏成千上萬個,你的服務就死了。

資源生命週期規則:取得 (Acquire) -> 使用 (Use) -> 保證釋放 (Release)
重點在「保證」,不管你看書時是睡著了、被隕石砸中、還是看到一半不想看了,你都得把書還回去。

經典案例:忘了還書

這是初學者寫的程式碼,假設一切順利的流程,借書、讀書、還書。

// 🔴 臭味:這就像說「我看完書一定會還」,但沒考慮任何意外。
async function read(path, fs) {
  // 1. 借書 (打開了資源)
  const fd = await fs.open(path, 'r');

  // 2. 讀書 (使用資源)。如果這本書是空白的或有問題(拋出錯誤)...
  const buf = Buffer.alloc(10);
  await fs.read(fd, buf, 0, 10, 0);

  // 3. ...你就嚇跑了,永遠忘了去還書。底下這行根本沒機會執行。
  await fs.close(fd);
  return buf;
}

程式碼一旦出錯,它會直接跳過後續所有步驟,包括你那個「還書」的 close() 呼叫。
結果就是,書(資源)永遠留在了你手上,沒人能再用了。

最低標準的寫法:手動使用 try...finally

try...finally 就是你寫給圖書館的切結書(絕對保證)

這份try 區塊切結書上寫著:「無論我在讀書時發生什麼事——就算我讀到一半中樂透跑了,或者書太無聊直接提前返回——我finally 區塊裡的程式碼 都會保證執行『還書』這個動作。」

// 🟡 至少是個負責任的人
async function read(path, fs) {
  let fd; // 必須把書先拿到外面,切結書才能看到
  try {
    // 1. 借書
    fd = await fs.open(path, 'r');

    // 2. 讀書
    const buf = Buffer.alloc(10);
    await fs.read(fd, buf, 0, 10, 0);
    return buf;

  } finally {
    // 3. 執行切結書:無論如何,只要我借到書(fd存在),就一定還回去!
    if (fd) {
      await fs.close(fd);
    }
  }
}

可以這樣用,而且不會毀了你的系統。
但每次借書都要寫一份這麼麻煩的切結書,也容易出錯。
https://ithelp.ithome.com.tw/upload/images/20250927/20124462NR0lYPU4Uu.png

更好的方式:打造「借還書機器人」(出借模式 Loan Pattern )

聰明的人會消除犯錯的味道,不會每次都手動,他們會設計一個讓錯誤無法發生的系統,讓還書這個動作自動化。
主要概念是:資源的生命週期綁定在一個物件的生命週期上

try...finally 這個細節封裝起來,變成「借還書機器人」 (withFile)。
你只要告訴這個機器人你想借哪本書、以及借到書後想做什麼事,剩下的全給它做就好。

這種「將資源的取得與釋放,包裹在一個函式或物件的生命週期內」的技巧,在程式設計領域被稱為「出借模式」(Loan Pattern) 或「環繞執行模式」(Execute Around Method Pattern)。

我們要參考這個精神:讓機器(程式結構)來保證資源的釋放,而不是靠人的記憶力。

// 這個「借還書機器人」,把所有繁瑣的細節都藏在裡面。
async function withFile(path, action) {
  let fd;
  try {
    // 機器人幫你借書
    fd = await fs.open(path, 'r');
    // 機器人把書交給你,讓你做你想做的事 (action)
    return await action(fd);
  } finally {
    // 你做完事後,機器人保證幫你還書
    if (fd) {
      await fs.close(fd);
    }
  }
}

// 🟢 好味道:你只關心讀書的內容,不用擔心還書的細節。
async function read(path, fs) {
  // 告訴機器人: 我要這本書(path),拿到後幫我執行這段讀書計畫。
  return withFile(path, async (fd) => {
    const buf = Buffer.alloc(10);
    await fs.read(fd, buf, 0, 10, 0);
    return buf;
  });
}

withFile 時,根本沒有機會忘記還書,物件生命週期結束時 (離開了作用域),它會被自動銷毀,此時就釋放資源(還書)。

這個將資源取得與釋放包裹起來的技巧,把資源生命週期管理這個複雜問題,抽象成一個簡單、可靠的工具。

🟢 好味道:借還書機器人 - 出借模式

https://ithelp.ithome.com.tw/upload/images/20250927/20124462zv6lTCCDRP.png

最終方案:讓語言成為你的機器人 (await using)

「出借模式」的精神非常棒,現代 JavaScript 甚至將這個模式內建到了語言本身。
using 的用法是:離開作用域時,可以使用 Symbol.dispose釋放掉任何內容。參考連結:using

而用於非同步資源則是 await using參考連結:await using
要使用這個特性,我們只需要讓資源物件符合可棄置資源(Disposable Resource) 規範,也就是提供一個名為 [Symbol.asyncDispose] 的清理方法。

// 建立一個會回傳「可自動關閉檔案」物件的函式
async function openFileResource(path, fs) {
  const fileHandle = await fs.open(path, 'r');
  console.log("檔案已開啟...");

  return {
    handle: fileHandle,
    // [規範核心] 定義非同步的清理方法
    [Symbol.asyncDispose]: async () => {
      await fileHandle.close();
      console.log("檔案已透過 asyncDispose 自動關閉!");
    }
  };
}

// 🟢🟢 頂級好味道:語法即保證,簡潔又安全
async function readModern(path, fs) {
  // 使用 await using 取得資源,語言會記住它需要被釋放
  await using file = await openFileResource(path, fs);

  // 專注於核心邏輯
  const buf = Buffer.alloc(10);
  await file.handle.read(buf, 0, 10, 0);
  return buf;

  // 完全不需要 try...finally 或 close() 呼叫!
  // 這才是真正的「自動除臭」
}

file 變數離開作用域時,無論是正常回傳還是中途出錯,[Symbol.asyncDispose] 方法都會被自動呼叫並等待完成
實現了「打開的東西,就要關掉,沒有例外」的最高境界。

https://ithelp.ithome.com.tw/upload/images/20250927/20124462FOb2KW9m0W.png

除臭大法

  • 手動保證:最低標準是養成使用 try...finally 的習慣。雖然繁瑣,但它能確保在各種情況下執行必要的清理工作。

  • 模式抽象:比手動更好的是,將邏輯封裝成可重用的輔助函式,也就是「出借模式」(範例withFile),能將「取得」和「保證釋放」的流程自動化。

  • 語法內建:最現代且可靠的方式,是利用 JavaScript 語言內建的 usingawait using。讓語法本身成為你的「自動除臭器」。

目標是盡可能地使用高層次抽象,讓機器(程式結構或語言本身)來保證資源的釋放,而不是依賴人的記憶力。

今日重點

  • 打開的東西,就要關掉,沒有例外。適用於檔案,也適用於資料庫連線網路請求交易 (Transaction)鎖 (Lock) 等任何有限的系統資源。
  • 最低的標準:try...finally 是手動確保資源釋放的基礎作法。
  • 把資源管理邏輯封裝成一個輔助函式,養成自我防呆的好習慣。
  • 現代的實踐:優先使用語言內建的 usingawait using,讓語法為你自動管理資源的生命週期,這是最安全、最簡潔的選擇。

一個好的程式設計師不是每次都記得防臭,而是發明了「自動除臭」。


上一篇
Day 24- 錯誤處理:別讓程式崩潰
系列文
消除你程式碼的臭味25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言