Init function 是 orchestrator、職責拆出獨立 function
Init function 是 orchestrator、職責拆出獨立 function
核心原則
單一函式做 ≥ 3 件無關的事就拆。 每個函式只負責一個職責、有明確的 input / output、可以獨立 debug 與測試。Init function 變成「組合各職責 function 的 orchestrator」。
為什麼拆函式
商業邏輯
單一函式做多件事的成本:
| 規模 | 維護痛點 |
|---|---|
| 一函式 50 行做 1 件事 | 低 — 容易讀、職責清楚 |
| 一函式 100 行做 3 件事 | 中 — 邏輯交織、debug 要分辨哪段 |
| 一函式 200 行做 5+ 件事 | 高 — 沒人想動、改一處可能影響別處 |
拆函式的成本是「多寫幾個函式名與簽名」、收益是「每個函式範圍小、debug 容易、可單獨重用」。
拆的依據是「職責」、不是行數
| 拆法 | 結果 |
|---|---|
| 按行數機械拆 | 切出沒邏輯意義的片段、更亂 |
| 按職責拆 | 每個函式名能描述「做什麼」 |
| 按 input / output 拆 | 函式變得 testable、可組合 |
按職責拆的判斷:能不能用一個動詞片語描述函式做什麼?做不到 → 多個職責、該拆。
這次任務的拆分機會
觀察
setupScopeFilter() 現況做 5 件事:
1function setupScopeFilter() {
2 // 1. 找元素
3 var scopeEl = document.querySelector('.search-scope');
4 var input = document.querySelector('.pagefind-ui__search-input');
5 // ...
6
7 // 2. 量測 scope 高度寫回 CSS 變數
8 function syncScopeHeight() { ... }
9 syncScopeHeight();
10 new ResizeObserver(syncScopeHeight).observe(scopeEl);
11
12 // 3. 把 filter-panel 搬到 sidebar (position function)
13 function place() { ... }
14
15 // 4. 註冊 scope filter listener + apply
16 function apply() { ... }
17 scopeEl.addEventListener('change', apply);
18 // ...
19
20 // 5. Reorder filter blocks
21 function reorderFilters() { ... }
22 reorderFilters();
23}5 個職責塞在一個函式:找元素、量高度、搬 slot、scope filter、reorder filter。
判讀
按職責拆成獨立函式:
1function findSearchElements(shell) {
2 return {
3 shell: shell,
4 ui: shell.querySelector('.pagefind-ui'),
5 input: shell.querySelector('.pagefind-ui__search-input'),
6 drawer: shell.querySelector('.pagefind-ui__drawer'),
7 filter: shell.querySelector('.pagefind-ui__filter-panel'),
8 scope: shell.querySelector('.search-scope'),
9 };
10}
11
12function syncScopeHeight(scopeEl) {
13 function update() {
14 var h = scopeEl.offsetHeight || 56;
15 document.body.style.setProperty('--search-scope-h', h + 'px');
16 }
17 update();
18 new ResizeObserver(update).observe(scopeEl);
19}
20
21function setupFilterSlotSwap(filter, drawer, slot, breakpoint) {
22 var mql = window.matchMedia('(min-width: ' + breakpoint + 'px)');
23 function place() {
24 if (mql.matches) slot.appendChild(filter);
25 else drawer.insertBefore(filter, drawer.firstChild);
26 }
27 place();
28 mql.addEventListener('change', place);
29}
30
31function reorderFilters(filterPanel, desiredOrder) {
32 var blocks = filterPanel.querySelectorAll('.pagefind-ui__filter-block');
33 var byKey = {};
34 blocks.forEach(function (b) {
35 var key = b.querySelector('.pagefind-ui__filter-name').textContent.trim().toLowerCase();
36 byKey[key] = b;
37 });
38 desiredOrder.forEach(function (k) {
39 if (byKey[k]) filterPanel.appendChild(byKey[k]);
40 });
41}
42
43function setupScopeFilter(scopeEl, input, ui) {
44 function getScope() { ... }
45 function apply() { ... }
46 function schedule() { ... }
47 scopeEl.addEventListener('change', schedule);
48 input.addEventListener('input', schedule);
49 new MutationObserver(schedule).observe(ui, { childList: true, subtree: true });
50}init() 變成 orchestrator:
1function init() {
2 var shell = document.querySelector('.search-shell');
3 if (!shell) return;
4
5 waitForElement(shell, '.pagefind-ui__drawer', function () {
6 var els = findSearchElements(shell);
7 syncScopeHeight(els.scope);
8 setupFilterSlotSwap(els.filter, els.drawer, document.querySelector('.search-filter-slot'), 1400);
9 reorderFilters(els.filter, ['type', 'tag']);
10 setupScopeFilter(els.scope, els.input, els.ui);
11 });
12}
13init();每個拆出的函式:
- 名字描述做什麼(動詞 + 名詞)
- 接受需要的元素當參數(不依賴全局)
- 不知道其他函式的存在(解耦)
內在屬性比較:四種函式拆分粒度
| 粒度 | 維護成本 | Debug 範圍 | 可重用性 |
|---|---|---|---|
| 一個 mega init function | 高 — 200+ 行交織 | 整個函式都要看 | 低 — 跟特定 setup 綁 |
| 按行數機械拆(每 30 行一份) | 中 — 切出無意義片段 | 中 | 低 |
| 按職責拆 | 低 — 每函式單一職責 | 函式內部、範圍小 | 高 |
| 按職責拆 + class 包裝 | 低 | 範圍小 | 最高 — 多實例 |
優先按職責拆 — 函式名表達 intent、debug 範圍小、單獨可測。
拆函式的具體技巧
1. 函式名是動詞片語
1syncScopeHeight() // 動詞 + 對象
2setupFilterSlotSwap() // 動詞 + 對象
3reorderFilters() // 動詞 + 對象
4findSearchElements() // 動詞 + 對象
不要:
1filter() // 動詞模糊(filter 是動詞還是名詞?)
2handle() // 太抽象
3init() // 只有 orchestrator 用、不要散在各處
2. 參數是該函式需要的、不傳一個 mega object
1// 好 — 函式知道它需要什麼
2function syncScopeHeight(scopeEl) { ... }
3
4// 較差 — 函式拿到一堆無關的東西、不清楚依賴
5function syncScopeHeight(allElements) {
6 var scope = allElements.scope;
7 ...
8}明確參數 = 明確依賴 = 容易測試。
3. 副作用集中在一處
1function syncScopeHeight(scopeEl) {
2 function update() {
3 document.body.style.setProperty('--search-scope-h', scopeEl.offsetHeight + 'px');
4 }
5 update();
6 new ResizeObserver(update).observe(scopeEl);
7}副作用(DOM 變動、event listener、observer)都在這個函式內。沒散到別處。
4. 不依賴外部變數
1// 好 — 純函式、依賴只在參數
2function reorderFilters(filterPanel, desiredOrder) { ... }
3
4// 較差 — 依賴外部全局變數
5var desiredOrder = ['type', 'tag'];
6function reorderFilters(filterPanel) {
7 // 用了 desiredOrder
8}純函式 = 無隱式依賴 = 重用方便、測試方便。
設計取捨:大型 init function 的拆分策略
四種做法、各自機會成本不同。這個專案選 A(按職責拆 + 純函式)當預設、其他做法在特定情境合理。
A:按職責拆 + 純函式 + init 當 orchestrator(這個專案的預設)
- 機制:每職責一個函式(動詞 + 對象命名)、依賴透過參數傳入、init 組合各函式
- 選 A 的理由:debug 範圍小(職責 = 函式 = grep 範圍)、單獨可測、可重用
- 適合:> 50 行的 init function、預期長期維護
- 代價:多寫幾個函式名與簽名、檔案 LOC 略增
B:按行數機械拆(每 30 行一份)
- 機制:固定 LOC 拆檔、不考慮職責邊界
- 跟 A 的取捨:B 拆完後切片無邏輯意義、A 切片各自完整;B 更亂、debug 反而更難
- B 是反模式:「行數」不是有意義的拆分判準 — 拆完後切片無邏輯意義、debug 反而更難
C:保持 mega init function
- 機制:所有 setup 邏輯塞在一個 init 內
- 跟 A 的取捨:C 一個函式看完所有 setup、A 散在多函式;但 C 在 200+ 行時改一處要小心整體
- C 才合理的情境:< 50 行的 init、職責本來就單一
D:按職責拆 + class 包裝多實例
- 機制:把 setup 包成 class、
new SearchShell(rootEl)建立實例 - 跟 A 的取捨:D 多實例支援更乾淨(每實例自己的 state)、A 用純函式 + 起點當參數也能達成
- D 比 A 好的情境:元件有複雜的內部 state、預期會被多次實例化(library 設計)
判讀徵兆
| 訊號 | Refactor 動作 |
|---|---|
| 一個函式 100+ 行 | 列出做的事、按職責拆 |
函式名抽象(init / handle / process) | 改名動詞 + 對象、表達 intent |
| 函式內讀外部全局變數 | 把依賴改為參數、純函式化 |
| Debug 時要 grep 整個函式找哪段邏輯 | 拆完後職責 = 函式 = grep 範圍縮小 |
| 同一段邏輯複製到別處 | 拆成獨立函式、兩處引用 |
核心原則:函式是「做一件事」的單位。一個函式越多職責、debug 與重用越難。拆 = 投資、回報是未來的維護成本下降。