つい先日、プロジェクト内に点在するハードコーディングを撲滅するタスクを行っていました。
撲滅しながらも「ハードコーディングが許されるパターンはないのか?」ということが気になり、調べてみました。
簡単にいうと、大事なのは、ハードコーディングされた値の意味・変更理由・影響範囲がコードから読み取れるか であり、場合によってはハードコーディングを許容してもいいのかもしれないと思えました。
硬編碼是指,直接把值寫進程式碼裡。
例如,以下的 80 就不容易看出意思。
if (score >= 80) {
return "passed";
}
這種值就稱為所謂的 魔術數字。
在這種情況下,如果替 80 加上一個表示其意義的名稱,意圖就會更容易傳達。
const PASSING_SCORE = 80;
if (score >= PASSING_SCORE) {
return "passed";
}
這樣一來,就能看出 80 不只是單純的數字,而是代表及格分數。
這種幫值命名的做法,即使該值只會使用一次,只要它在業務上有重要意義,我也認為是有價值的。
const MAX_LOGIN_ATTEMPTS = 5;
在這個例子裡,重點不是重複使用 5。
而是要能在程式碼中看出,5 代表「最大登入嘗試次數」。
也就是說,將值定義成常數,也是 賦予數值意義 的手段之一。
以下這類值也不建議直接寫死在程式碼中。
const apiUrl = "https://prod.example.com";
像 API URL 這種值,可能會因為開發環境、測試環境等不同環境而改變。
如果各環境的值不同,將其抽出成環境變數或設定檔會比較自然。
const apiUrl = process.env.API_BASE_URL;
特別是以下這些值,應該盡量避免硬編碼:
這些值如果直接寫進程式碼,不但難以修改,也可能引發資安問題。
不過,也不是所有字面值都一定要轉成常數。
例如,在測試程式碼中像下面這樣寫時,201 作為 HTTP 狀態碼,其意義相對明確。
expect(response.status).toBe(201);
如果每次都寫成下面這樣,反而有時會變得更難讀。
const CREATED_STATUS_CODE = 201;
expect(response.status).toBe(CREATED_STATUS_CODE);
當然,這還是取決於專案方針與上下文。
但如果把局部且意義明確的值也全部常數化,閱讀的人反而要多花工夫去追查定義來源。
在測試程式碼中,直接寫期待值有時會更容易閱讀。
expect(user.name).toBe("Taro");
expect(user.age).toBe(20);
這類值若能直接作為該測試案例的輸入或期待值在當下讀懂,通常會比較清楚。
另一方面,在測試中若是與業務規則相關的值,還是建議定義成常數。
const RESERVATION_CAPACITY_LIMIT = 10;
expect(result.capacity).toBe(RESERVATION_CAPACITY_LIMIT);
在進行常數化時,放置位置也很重要。
有一種做法是建立 constants.ts,把常數都集中管理。
一開始看起來很方便。不過隨著專案變大,這些常數是做什麼用的就會變得不容易看懂。
// constants.ts
export const ADMIN = "admin";
export const LIMIT = 10;
export const DEFAULT_NAME = "guest";
這種大型共用常數檔,反而可能讓依賴關係變得不清楚。
建議先放在使用的地方附近。
const MAX_LOGIN_ATTEMPTS = 5;
function canRetryLogin(attemptCount: number): boolean {
return attemptCount < MAX_LOGIN_ATTEMPTS;
}
等到真的有多處共享的需求時,再考慮共用化。
如果是只在特定領域使用的常數,也可以建立一個能看出用途的 constants.ts。
domain/
├─ user/
└─ post/
├─ index.ts
└─ constants.ts // 只在 post 使用的常數放置處
如果猶豫要不要硬編碼,可以從以下幾點來判斷:
只要符合其中一項,就值得考慮常數化、設定化或環境變數化。
我原本一直認為,硬編碼是應該被徹底消滅的壞東西。這次反過來思考什麼情況下可以被允許,也算是動腦筋的一種練習。
這次雖然沒有把它列成條目,不過如果是想先快速做出來驗證,或是遇到超緊急狀況必須先修正並發布(後續修正也是必要的)等等,依情況來說也可能是可以接受的。
實務上還是依照團隊和專案方針來決定吧。