嗨~ 歡迎閱讀第 53 期的 ExplainThis 全端雙週報。
在進到這期的主題文之前,想分享 ExplainThis 的《給工程師的 Cursor 工作流 — 透過 AI 代理全方位提升開發生產力》線上課程,在這週新增了 MCP 系列共 7 個單元的內容,涵蓋了 MCP 基礎介紹,到如何在使用 Cursor 時搭配 MCP。
先前我們有將搭配的教材上傳到 ExplainThis 的網站,讓想了解如何高效使用 Cursor 的讀者們可以免費閱讀 (連結);接下來我們也會將 MCP 的搭配教材陸續上傳,敬請讀者們期待。
先前我們有將搭配的教材上傳到 ExplainThis 的網站,讓想了解如何高效使用 Cursor 的讀者們可以免費閱讀 (連結);接下來我們也會將 MCP 的搭配教材陸續上傳,敬請讀者們期待。
如果偏好看影片課程的讀者,歡迎加入 E+ 成長計畫 (連結)。雖然這門課是以 Cursor 做為主要工具,但課程中講到的概念,都適用其他 AI 驅動的工具 (例如 Windsurf 或 GitHub Copilot)。
控制反轉 (Inversion of Control) 提高程式碼可維護性
相信多數讀者聽過依賴注入 (Dependency Injection) 這種「透過傳入參數,來做到解耦合,避免程式碼出現越來越多寫死的肥厚邏輯」。事實上有另一種也是透過傳參數,來避免某個方法或函式內的邏輯過於肥厚、難維護。
一樣讓我們先透過一個例子來理解 (此案例來自 Kent C. Dodds 的分享)
// filter 函式,可以根據不同條件過濾元素
function filter(array, {
filterNull = true,
filterUndefined = true,
filterEmptyString = false,
} = {}) {
let newArray = []
// 迭代過陣列
for (let index = 0; index < array.length; index++) {
const element = array[index]
// 每一輪的迭代,會根據這個條件判斷,來根據參數決定是否要過濾某類型的元素
if (
(filterNull && element === null) ||
(filterUndefined && element === undefined) ||
(filterEmptyString && element === '') ||
) {
continue
}
newArray[newArray.length] = element
}
return newArray
}
// 具體用法如下,根據傳入的不同條件來過濾
let input = [0, 1, undefined, 2, null, 3, 'four', '']
filter(input) // [ 0, 1, 2, 3, 'four', '' ]
filter(input, {filterNull: false}) // [ 0, 1, 2, null, 3, 'four', '' ]
filter(input, {filterUndefined: false}) // [ 0, 1, undefined, 2, 3, 'four', '' ]
讀者們看完上面這段程式碼,覺得會難維護的原因在哪呢?
相信多數人有發現,這個 filter
函式之所以難維護,是因為當今天如果要新增一個過濾的邏輯,就需要在現在已經有多行的 if
條件句底下,再多寫一個條件判斷。例如假如要把 0
過濾掉,就要再多加上 (filterZero && element === 0)
。如果有越來越多不同條件,整個 if
就會變超級巨大,很難維護。
什麼是控制反轉 (Inversion of Control)?
要解決上面的問題,控制反轉 (Inversion of Control 或簡稱 IoC) 是常會用到的手段。一樣我們先來看看重構後的程式碼,再來談什麼是控制反轉。
如果要重構上面的程式碼,我們可以這樣改寫
function filter(array, callback) {
const result = [];
// 迭代過陣列
for (let i = 0; i < array.length; i++) {
// 取出迭代的元素
const element = array[i];
// 透過 callback 來決定是否要過濾
if (callback(element, i)) {
result.push(element);
}
}
return result;
}
可以觀察到,上面這個寫法的做大區別,是傳入 callback
,這個 callback
讓我們可以很靈活的決定要過濾掉哪種類型的元素。
看到下面的用法,如果要把 null
或 undefined
過濾掉,只要傳入 element !== null && element !== undefined
的過濾條件即可。同理,如果要過濾掉除了字串以外的元素,只要傳入 typeof element === 'string'
即可。
let input = [0, 1, undefined, 2, null, 3, "four", ""];
const removeNullAndUndefined = (element) =>
element !== null && element !== undefined;
filter(input, removeNullAndUndefined); // [ 0, 1, 2, 3, 'four', '' ]
// 只保留字串
const keepOnlyStrings = (element) => typeof element === "string";
filter(input, keepOnlyStrings); // [ 'four', '' ]
事實上,這個重構的版本,是我們在 函式程式設計 (functional programming) — 高階函式 主題文談過的。但當時是從高階函式的角度切入,而在這篇主題文則從反轉控制的角度來談。
所謂的反轉控制,如字面上的意思,是把控制權反轉過來,從原本由函式來控制邏輯,反轉成在函式以外控制邏輯。
這樣聽起來很抽象,讓我們用 filter
的例子來理解。在原本版本的 filter
,邏輯都是寫在 filter
裡面,所以是由filter
函式本身來控制邏輯。而重構的版本,過濾與否是由 callback
來決定,而這個 callback
是從外傳進來的,所以在這個狀況下,控制權被反轉到函式之外。
反轉控制的本質是什麼?
在理解完反轉控制後,讓我們試著從本質的角度來看反轉控制。
第一個想談的本質,是函式或方法本身,不再追求控制,不再由函式或方法本身,來決定該怎麼做。而是挖一個空,讓使用函式的人決定。以 filter
來說,用的時候再決定要傳什麼 callback
,不必在寫 filter
函式時就決定。
第二個是保留核心,因為今天把控制交由傳入的 callback
來決定,filter
的核心可以保持不變。今天不管要怎麼樣過濾,都可以完全不用去動 filter
,只用傳一個不同的 callback
即可。
當把上面兩個本質合在一起看,就會發現反轉控制讓程式碼變更好維護。因為一來因為可以靈活的傳入不同 callback
,整體的靈活性變好了,不用因為想要有不同的過濾邏輯而要苦惱如何改程式碼。二來核心的東西不變,意味著要維護的部分變少了,因為可以很確信最核心的 filter
可以不用去管。
本期推薦
近期最受社群關注的 AI 驅動 IDE 的執行長,分別到不同的 podcast 受訪,聽了幾集覺得都很有洞見。比起看社群中一些沒有深思的發言談 AI 對工程師的影響,看這些把時間全部投注在相關領域的分享,也會對這領域未來發展有比較實際的理解。Cursor 執行長訪談 (連結)、Windsurf 執行長訪談 (連結)、Devin 執行長訪談 (連結)
上面談到 Devin 背後的公司 Cognition Labs 最近也推出的 DeepWiki,詳細拆解 GitHub 的開源專案。只要把開源專案的 GitHub 網址前面改成 deepwiki.com,就會獲得 AI 對該專案的分析。如果有不清楚的,還可以直接問 AI。這對於入門理解一個開源專案的幫助很大 (連結)
上週 Cory House 寫了《How to write bad software》 裡面談到會把軟體寫糟的方式,推薦讀者們務必要避免 (連結)
這週看到 Martin Casado 發了一個在社群中被廣傳的推文,他提到在電腦科學的領域,要在自己所專長的領域往下多學一層。舉例來說,如果是作業系統的開發者,要去理解硬體層;如果是系統的開發者,要去理解作業系統層;如果是應用程式的開發者,則需要去理解系統層。這個洞見非常精闢,在 AI 輔助開發的時代下,依然適用 (連結)
前陣子看到一個完整度很高的前端開源專案,是 youtube-music 這個復刻 YouTube Music 客戶端的專案。推薦想要做個人專案的前端工程師讀者,可以把這種程度作為標竿,能讓自己的專案更突出 (連結)
另一個最近看到覺得非常酷的開源專案,是 beatsync 專案,讓不同裝置能夠同時同步播放,形成環繞音響的效果 (連結)
過去兩年越來越多公司從全遠端、混合模式,逐漸走回全辦公室工作的模式,然而新的研究結果卻發現,在家工作讓人更有效率也更專注 (連結)
文末彩蛋
先前看到 Jack Altman 講得一句話,很精闢地點出了兩種不同看待職涯的思維。他說「有些人將每個遇到的人都視為競爭對手,另一些人則把每個遇到的人都視為潛在合作對象」,兩種思維誰比較容易成功可能難有定論,但是顯然後者會活得比較快樂。
Some people view everyone as a potential competitor and others view everyone as a potential collaborator.
I'm not sure which is more successful on average, but the collaborators certainly seem happier.
— Jack Altman
這句話讓人很有感的地方是,不論在教育或者求職,很常會遇到名額有限,所以必須跟身旁的人競爭才得以脫穎而出,這很容易培養出競爭的心態。然而用這種思維方式,即使不斷往上爬,也將會失去在過程中因為與他人合作所獲得的情誼與快樂;而這種快樂往往會是在回首職涯時仍會記得的。
退一步說,在軟體業的工作中,想成就影響力大的專案,往往需要有跨團隊、跨部門的協作。因此比起總在思考如何鬥過身旁的人,去思考如何有效與他人合作,反而更容易讓影響力大的專案成功。
不論是以創造美好回憶出發,或者是以創造更大的影響力出發,願我們都能以合作的角度,看待職涯所遇的每一個人、每段關係。