先記住:SOLID 用來解決具體變更風險,不是湊原則。
需要深入時:再做介面與依賴重構。
假設 AI 把購物車整理成一堆 class、factory 和 interface,結果改個優惠規則還是得讀完整個專案。
這時不該先問用了幾個設計原則,而是原本的變更困難到底有沒有減少。
今天先嘗試把 SOLID 當成幾個審查問題,就不當成一定要全部套上的模板。
對前端來說,SOLID 不一定要用 class 或繼承來實作。
它比較像一組幫助我們檢查責任、擴充、替換、介面與依賴的問題。
重要的是降低不必要的連動,而不是讓程式看起來符合某種教科書格式。
教學情境:checkout 流程直接匯入 httpQuoteService,讀取它特有的巢狀欄位。
換測試替身或新服務時,流程也跟著改。
SRP > 把報價傳輸與結帳協調分開。
ISP > checkout 只需要取得報價,不必依賴同時包含登入、上傳圖片與分析事件的巨大服務。
DIP > 則要求高階流程依賴它需要的抽象能力,而不是外部實作的細節。
DIP > 可以先理解成「核心流程不要直接依賴外部細節」。
Adapter 是轉接頭,負責把後端格式翻譯成內部格式
DIP 則決定依賴方向:checkout 只認得自己需要的 QuotePort,HTTP Adapter 去遵守這個介面。
這樣後端欄位改名時,變動會集中在 Adapter,而不是一路擴散到畫面和流程。
可以由流程附近定義 QuotePort:它接受購物車快照,回傳領域報價或約定錯誤。
HTTP Adapter 實作這個能力,組裝處再把它交給 checkout。
執行時仍然是 checkout 呼叫取得報價。
但原始碼不再要求 checkout 認識 HTTP Adapter;反而是 Adapter 遵守流程需要的介面。
僅僅加一個 handler,如果 handler 還把外部 DTO 原封不動交出去,就沒有真正隔離外部變動。
TS interface 也不是執行期驗證器,外部資料仍需在邊界檢查。
以下只是依賴關係示意,可別拿來執行:
import { httpQuoteService } from './httpQuoteService'
const quote = await httpQuoteService.fetch(cart)
// QuotePort 由使用它的流程定義,實作在組裝處注入
const quote = await quotePort.getQuote(cart)
重要的不是變數改名,而是 quotePort 的輸出已經是流程理解的報價,包含一致的版本與錯誤語意。測試替身也必須遵守相同約定。
依賴注入是把需要的物件或函式從外面傳入的手法;DIP 是依賴應朝向穩定抽象的原則。
兩者常一起使用,但不是同義詞。
依賴注入簡介:前端中文文章
OCP 可以提醒我們把已知會替換的報價來源放在可擴充邊界,但不表示任何新需求都不能修改舊程式。
LSP 則要求替代實作維持呼叫者需要的契約。
若測試替身永遠同步成功,真實實作會非同步失敗,就容易掩蓋重要行為。
方法名稱相同,不等於行為可替換。
不要急著記名詞,只看「哪裡容易一起壞」:
| 原本的寫法 | 遇到什麼問題 | 改成白話 |
|---|---|---|
| 結帳流程直接呼叫某個 API | API 改格式,結帳和畫面都要跟著改 | 中間放一個小轉接層(Adapter) |
| 畫面自己讀後端的巢狀欄位 | 每個元件都知道後端細節 | 先翻成前端看得懂的報價資料 |
| 測試要模擬真正的網路服務 | 測試很難寫,也綁住傳輸方式 | 測試只提供一個符合規則的假服務 |
這裡的重點只有一句話:讓結帳流程只處理結帳,不要同時處理 API 格式。
請先指出 checkout 直接依賴 HTTP 實作造成的具體變更風險。依 SRP、ISP、DIP 提出最小重構:由流程定義 QuotePort,HTTP Adapter 負責資料驗證與轉換,在組裝處注入。保持現有功能與錯誤語意;不加入容器或繼承框架。列出真實與測試實作共同遵守的契約,驗證成功、業務拒絕、逾時及版本不一致。
問自己:明天換掉報價來源,哪些檔案應該不受影響?
如果說不出來,先畫依賴方向,再請 AI 重構。
今天的重點不是讓程式看起來像教科書,而是試著讓一個合理變動有可控制的影響範圍。
我覺得這篇連我自己都要反覆看好幾遍才要知道怎麼使用,但盡量先把名詞白話列出來該需要用什麼就用什麼。
明天會檢查拆開之後,是否仍然暗中耦合。