把變動頻繁的設定從穩定的主要邏輯裡拿出去。
你的核心商業邏輯,應該像一個在無菌室裡工作的科學家。
它不應該知道、也不應該關心外界的天氣(執行環境)如何。
需要的資料(設定),應該由一個助手(應用程式的啟動層)準備好,然後從門口遞進來。
一個函式或模組的簽名(signature),就是它對外部世界立下的契約(contract)。
設定外置,邏輯內聚。
每個函式的簽名 (Signature),就是它向外界立下的 契約 (Contract)。
一份好的契約,必須是誠實、公開且不含任何隱藏條款的。
拿一個簡單指令 cp
來舉例。
它的用法是: cp <來源檔案> <目標路徑>
。
這行簡單的文字,就是它與你簽訂的契約。
cp
來源檔案
和 目標路徑
這兩項資訊。這個契約是誠實且公開的,建立了我們對它的信任。cp
指令非常可靠,它絕不會在你背後偷偷讀取某個全域設定檔,或是根據今天的日期決定要不要多複製一份。
它只做契約上寫明的事,並且信守承諾。
讓一個應該只負責「連線」的函式,同時也承擔了「讀取設定」的職責。
// 🔴 臭味:它的契約隱藏輸入,職責混淆
async function connect() {
// 它偷偷從外部世界拿了一個全域變數。
// 這是一個隱藏的、未在簽名中聲明的依賴。
const url = process.env.DB_URL;
return await open(url);
}
不誠實的契約
它無法被正常測試: 我想測試這個 connect
函式嗎?我現在必須去污染全域的 process.env
物件。這是一種很髒的測試方式。好的測試應該是隔離的,不需要去修改那些全域共享的狀態。
它沒有彈性: 如果想在同一個程式裡,臨時連線到一個不同的測試資料庫怎麼辦?我辦法簡單地傳一個不同的 URL 給 connect
。必須去修改整個程式的執行環境。
它不誠實: 它隱藏了「我需要一個資料庫 URL」這個重要的依賴關係。
把依賴關係從隱含的秘密,變成明確的契約條款。
// 🟢 好品味:這是一個誠實的函式。它的契約清晰、完整。
// 這個函式現在很純粹。給它什麼設定,它就用什麼設定。
// 它 100% 遵守了它的簽名契約。
async function connect(dbUrl) {
return await open(dbUrl);
}
// --- 在應用程式的最外層 (entry point / 組裝處) ---
// 這是唯一一個需要處理「髒活」的地方。
// 它負責從外部世界讀取設定,然後把它們組裝好。
const config = {
dbUrl: process.env.DB_URL,
// ... 其他所有設定
};
// 然後,把乾淨的設定物件,作為契約的一部分,注入到我們誠實的函式中。
connect(config.dbUrl);
誠實的契約
誠實的契約: connect(dbUrl)
這個簽名清晰地宣告了它的依賴:「你需要提供我一個 URL,我才能工作。」沒有秘密,沒有隱藏的魔法。
可預測且可測試: 可以輕鬆地測試這個函式:connect('fake_db_url_for_testing')
。
不需要碰任何全域變數,測試變得簡單、快速、可靠。
關注點分離: 有個清晰的邊界。
應用程式的啟動層(最外層)負責處理所有與環境相關的「髒活」。
應用程式的主要邏輯層,則是完全乾淨、與環境無關的,它們只透過誠實的契約(函式參數)來溝通。
依賴注入(Dependency Injection):任何需要設定的函式,都必須把那個設定結構當成參數明確地傳給它,而不是在函式中直接讀環境。
設定層:應用程式啟動的第一件事,就是從環境變數或設定檔裡把所有需要的「資料」讀進來,塞進一個簡單的結構(struct)裡。讀完之後,就再也不要去碰那些外部的東西。
預設值與覆蓋:本地有預設,環境可覆蓋;缺值時清楚報錯。
重點就一個:把會變的東西(資料)和不會變的東西(邏輯)分開。