異步寫入(Async):三道菜同時做

比喻:餐廳廚房。

你在餐廳點了三道菜:沙拉、牛排、甜點。

同步做法:廚房做完沙拉 → 端出來 → 做牛排 → 端出來 → 做甜點 → 端出來。你等到天荒地老。

異步做法:三道菜同時開工!沙拉區切菜、烤爐烤牛排、甜點師做提拉米蘇。誰先做好誰先上桌。效率大幅提升。

程式的世界也是這樣。同步(Synchronous)是一件事做完才做下一件;異步(Asynchronous)是多件事同時做,誰先完成誰先回報。

用文字畫出來大概長這樣:

同步流程(一件一件來)

[查使用者資料] ──等 2 秒──> 完成
              [查訂單紀錄] ──等 3 秒──> 完成
                           [寄通知信] ──等 1 秒──> 完成

總共花了:2 + 3 + 1 = 6 秒

──────────────────────────────

異步流程(同時開工)

[查使用者資料] ──等 2 秒──> 完成 ┐
[查訂單紀錄] ──等 3 秒──> 完成 ├─> 全部到齊,繼續!
[寄通知信]  ──等 1 秒──> 完成 ┘

總共花了:3 秒(取最慢的那個)

看起來異步完勝?沒錯,大部分情況下異步確實更快。但問題來了——

異步的坑:順序不一定對

回到餐廳的比喻:如果甜點比牛排先做好,但甜點的淋醬需要用牛排的肉汁呢?甜點先到你桌上,但它還沒淋醬——這就出問題了。

程式裡的真實案例:

你寫了一個「註冊流程」:
1. 在資料庫建立使用者帳號
2. 用這個帳號的 ID 去建立預設設定
3. 寄歡迎信給使用者

如果三步同時跑(異步),第二步可能比第一步先完成——但帳號還沒建好,哪來的 ID?

結果:程式爆掉,因為它找不到一個還不存在的帳號。

Race Condition:搶最後一個座位

Race Condition(競爭條件)是異步最經典的問題。想像這個場景:

電影院最後一個座位。小明和小華同時在不同的電腦上訂票。兩個人幾乎同一秒按下「訂票」。系統查了一下:「有一個空位?有!」→ 兩個人都成功訂到了。但實際上只有一個座位。

沒處理 Race Condition
小明查:有 1 個空位 → 訂!
小華查:有 1 個空位 → 訂!

結果:2 個人搶到 1 個座位
有人到現場沒位子坐
有處理 Race Condition
小明查:有 1 個空位 → 鎖住 → 訂!
小華查:座位被鎖 → 等一下...
小明完成 → 解鎖 → 空位 = 0
小華查:沒位子了 → 顯示已售完
正確!一個座位只賣一次
跟 Claude 怎麼說
當你的系統開始有「多人同時操作」的需求時,記得跟 Claude 說:
「這個功能可能有 race condition,幫我加上 lock 機制」或
「這幾個步驟有順序依賴,不能用 Promise.all,要依序執行」

SSOT:全公司只有一本電話簿

SSOTSingle Source of Truth 的縮寫,中文叫「單一真理來源」。聽起來很哲學,但其實超級實用。

比喻:電話簿。

假設公司有三個部門,每個部門各自維護一份員工電話簿。

小明換了手機號碼,HR 部門更新了,但業務部和工程部還是舊號碼。
三個月後,業務部打小明電話打不通,客戶的案子卡住了——

問題出在哪?同一筆資料存了三份,只更新了一份。

程式裡也是一模一樣的問題。來看一個實際案例:

實際問題:商品價格存了三個地方

沒有 SSOT — 三份資料各自為政
前端 JavaScript:
const price = 299; // 寫死在程式裡

後端 API:
return { price: 350 }; // 上次改了忘記通知前端

資料庫:
product.price = 399; // 最新的價格

結果:客戶看到 299,結帳變 350,
帳務對帳是 399。三個數字都不一樣。
有 SSOT — 只存一個地方
資料庫(唯一真相):
product.price = 399;

後端 API:
return db.getProduct(id); // 去 DB 查

前端:
const data = await fetch('/api/product');
// 顯示後端回傳的價格

結果:不管在哪裡看,都是 399。
改一個地方,全部同步更新。

SSOT 的原則很簡單:每一筆資料只存在一個地方,其他需要這筆資料的地方,都去那裡查。

SSOT 的常見應用場景

場景 真理來源(SSOT) 其他地方怎麼做
商品價格 資料庫 前端、後端都去 DB 查
使用者權限 後端的權限表 前端只負責顯示,不自己判斷
設定值 環境變數 / 設定檔 程式啟動時讀取,不寫死在程式裡
UI 狀態 狀態管理工具(如 Redux) 各元件去 store 讀取,不各自維護
跟 Claude 怎麼說
「這個資料的 SSOT 是資料庫,前端不要自己存一份」
「幫我檢查一下,有沒有同一筆資料被存在多個地方的情況」

耦合(Coupling):積木 vs 膠水模型

比喻:積木和膠水。

想像你蓋了一個模型城堡:

用積木蓋的(低耦合):每一塊都是獨立的。你拆掉一棟塔樓,城牆還是好好的。想換一個更酷的塔樓?直接拔掉舊的、插上新的。

用膠水黏的(高耦合):所有零件都黏死在一起。你想拆掉一棟塔樓?不好意思,旁邊的城牆、護城河、吊橋全部一起碎掉。因為它們都黏在一起了。

軟體的世界裡,「耦合」就是指兩個功能之間的依賴程度。依賴越深,耦合越高;依賴越少,耦合越低。

高耦合的痛:改 A 壞 B C D

這是新手最常遇到的地獄場景。你跟 Claude 說「幫我改一下登入頁面的按鈕顏色」,結果改完之後:

為什麼?因為這些功能全部擠在同一個檔案裡,共用同一段 CSS,牽一髮動全身。

高耦合 — 所有東西攪在一起
一個 app.js 檔案裡面塞了:
- 登入邏輯
- 商品列表
- 購物車計算
- 寄信功能
- 金流串接

改登入 → 購物車壞了
改金流 → 寄信功能掛了
全部糾纏在一起,無法單獨維護
低耦合 — 功能模組各自獨立
每個功能獨立一個模組:
- auth/login.js  → 只管登入
- products/list.js → 只管商品
- cart/calculate.js → 只管購物車
- email/send.js  → 只管寄信
- payment/stripe.js → 只管金流

改登入 → 其他功能不受影響
改金流 → 寄信功能照樣運作
各模組獨立,改一個不會壞另一個

怎麼降低耦合

比喻:部門之間用公文往來。

公司裡,業務部需要工程部做一個功能。正確做法是寫一張需求單(API),工程部按照需求單來做。

錯誤做法是業務部的人直接跑去工程部的桌上翻資料、改程式碼。這樣工程部根本不知道什麼被改了,下次部署直接爆炸。

模組之間的溝通,應該透過明確定義的介面(API),而不是直接互相觸碰內部邏輯。

降低耦合的三個基本原則:

  1. 功能拆成獨立模組:每個檔案只做一件事。登入歸登入、購物車歸購物車。
  2. 模組之間用 API 溝通:A 模組需要 B 模組的資料?透過 B 暴露的 function 來拿,不要直接去改 B 的內部變數。
  3. 不要共用全域變數:就像不要讓所有部門共用一張桌子。每個模組管好自己的資料。
跟 Claude 怎麼說
「這個功能跟其他模組耦合太高了,幫我拆成獨立的 module」
「我不想改 A 的時候影響到 B,幫我用 interface 把它們解耦」
「這個檔案太大了,幫我按功能拆分」

前端 vs 後端:什麼計算該放哪裡

這是很多新手搞不清楚的問題:某段邏輯到底該寫在前端還是後端?

比喻:餐廳的菜單 vs 收銀台。

菜單上可以印「參考價格」,讓客人大概知道要花多少錢。但結帳金額一定要收銀台算

為什麼?因為菜單在客人手上,客人可以塗改。但收銀台在店裡面,客人碰不到。

前端 = 菜單(在使用者的電腦上,使用者看得到、改得到)
後端 = 收銀台(在伺服器上,使用者碰不到)

判斷原則:誰能被竄改?

前端的程式碼跑在使用者的瀏覽器裡。任何懂一點技術的人,都可以打開瀏覽器的開發者工具,修改前端的 JavaScript。所以——

類型 放前端 放後端
日期格式化 可以。純展示,沒安全問題 不需要
表單欄位驗證 可以做,提升使用者體驗 一定要!前端驗證可以被繞過
訂單金額計算 可以顯示「預估金額」 一定要!最終金額只能後端算
使用者權限判斷 不行!使用者可以改前端 一定要!權限只能後端決定
密碼加密 不行!密碼不能在前端處理 一定要!
搜尋結果排序 可以做客戶端排序 資料量大時要在後端排序
關鍵原則
涉及「錢」、「權限」、「安全」的邏輯,一定要放後端。

前端可以做「預覽」和「使用者體驗優化」,但最終的計算和判斷,必須在伺服器上完成。

記住:前端是給使用者看的,後端是給你自己用的。使用者看得到的東西,都有可能被竄改。

一個真實的災難案例

某購物網站把折扣計算寫在前端 JavaScript 裡。結果有人用瀏覽器的開發者工具,把折扣從「打九折」改成「打一折」,然後送出訂單。後端沒有重新驗算,直接按前端傳來的金額扣款。

一台原價三萬的筆電,用三千塊就買走了。

跟 Claude 怎麼說
「這個折扣計算要放在後端做,前端只顯示結果」
「幫我確認所有跟金額有關的邏輯都在 server side」
「前端的權限檢查只是 UX,後端的 middleware 才是真正的守門員」