Published on

《A Philosophy of Software Design》心得 I — 寫出複雜度低的軟體

目錄

還記得剛入軟體工程師這行時,在熬過了前期熟悉公司程式庫,脫離了只能改改文案、寫一些簡單的函式後,我終於被分派到一張估時三天的任務。那時我心裡想「終於是我好好發揮,證明自己能寫出點東西的時候」。在經過兩天多的努力,我總算發出了 PR (pull request)。因為自己有先測試過,所以覺得應該沒問題,可以順利通過 code review 吧 (?)

結果我完全大錯特錯!那次的 code review 收到將近二十處的評論。沒錯,快要二十個評論!其中不乏「這邊為什麼需要這個?」、「為什麼這裡這樣寫?」公司裡嚴謹的資深前輩在 Gitlab 上的提問。在收到這些反饋後我重新思考,發現的確我提交的第一個版本,充滿了冗餘的程式,或是可以被精簡的邏輯,甚至有著我自己沒有意識到,但卻會影響到其他程式的地方。

好險當時有前輩的把關,現在回頭看,要是當時沒有 code review 直接讓我寫的那些程式進到程式庫中,不知道會對整體程式庫的健康程度損害多少 😓。不過也是在那次的 code review,我看到了自己與資深工程師的距離。

最近在讀史丹佛大學教授 John Ousterhout 前幾年出的 《A Philosophy of Software Design》一書,有系統地歸納軟體設計常見的問題。這時我才想到當年的我,雖然我寫出來的東西,功能上沒有問題,但是在「複雜度 (complexity)」上卻大有問題!

《A Philosophy of Software Design》

什麼是複雜度 (complexity)?

談到軟體開發的複雜度,有幾種看待的角度,舉例來說演算法很常會用時間複雜度 (time complexity) 以及空間複雜度 (space complexity) 來衡量演算法的效能。不過這篇文章想探討的複雜度,不是時間與空間複雜度,而是軟體設計有多易懂與易改。

在《A Philosophy of Software Design》一書當中,作者 John Ousterhout 談到,如果某段程式沒辦法輕易讓其他人讀懂,那就是太複雜了;如果要修改某段程式,需要同時修改到許多其他地方,那也是太複雜了;或是要修某段程式碼,卻在改的過程引出另一個 bug,這也代表原本的寫法複雜度太高。

假如你也有過類似以下的經驗大概會更有感 — 自己寫的程式碼明明怎麼看都沒問題,但卻仍是報錯,最後一路往下追,才發現是因為某段歷史遺跡 (legacy code) 導致 (於是腦中怒罵前人怎麼埋了個炸彈)。這種隱藏式炸彈,可說是軟體設計複雜度太高的極致表現 (當然這種極致是我們必須盡可能避免的)。

反過來說,假如你寫的程式別人一看就懂、改了不會影響到其他程式碼、改了不會引發其他錯誤,那麼恭喜你,你寫的程式複雜度是低的。在軟體設計有個原則叫鬆散耦合 (loosely-coupled),許多人也會說「低耦合」,這是指軟體之間的關聯性低 (換句話說改了 A,不會影響到 B)。當遵照低耦合原則,通常也會設計出複雜度低的軟體。

複雜度過高的三大病癥

《A Philosophy of Software Design》有進一步談到三個複雜度過高的表徵,當寫程式時遇到下面三種狀況,就是需要警覺的時候!

改動時需要大量改動

試想有一個網站有十多個頁面,頁面中都有用到某一個主題色,而在第一版的設計中,設計師請前端工程師把主題色設成亮紅色,但是到了第二版,設計師覺得不行,要暗紅色會比較好看。假如這個主題色,是被分別寫到十多個頁面中將近五十個元件中,那麼當要改動顏色時,就需要分別去改五十多個地方。

像是下面這樣,如果要改顏色,要去三個頁面分別改:

// A 頁面中
<Button color="#ff5050"/>
// B 頁面中
<Box backgroundColor="#ff5050" />
// C 頁面中
<SnackBar color="#ff5050" />

若要解決這個問題,可以把主題色抽出來,讓我們只需改一個地方,就能全都改:

// 定義好主題色,讓其他地方飲用
// 之後只要改主題色,其他地方都會改,因此只需改一個地方即可
const primary = "#ff5050"
// A 頁面中
<Button color={primary}/>
// B 頁面中
<Box backgroundColor={primary} />
// C 頁面中
<SnackBar color={primary} />

讓其他開發者認知負擔過重

這邊的認知負擔是指「事先需要知道多少先備知識」,而認知負擔過重則代表,其他開發者如果想要讀懂某段程式碼,會需要先了解的先備知識過高。打個比方來說,假如用一台微波爐之前,要先去了解微波爐背後的物理原理,或是需要先了解微波爐在製造過程的細節,那麼多數人應該都不會知道該怎麼用一台微波爐。這就是所謂認知負擔過重。

不過好加在,現代的微波爐設計簡單到小朋友都能夠輕易操作,這種讓操作者不須知道太多先備知識,可以說是好設計的典範。同樣的道理,在設計程式時,如果設計的讓其他協作者,需要花很多時間去了解,那這時的複雜度就會是過高的。

舉實際的例子來說,許多程式語言會需要處理記憶體洩漏 (memory leak) 的問題,假如你寫了某個函式,在使用時需要額外有步驟再去釋放記憶體,這時使用該函式的人,很可能會因為忘了釋放記憶體,而造成記憶體洩漏;然而,如果能把該部分的程式設計成,用的人不用處理記憶體釋放,而是程式會自動做掉那塊,那麼對用的人來說,就會比較簡單 (換句話說,複雜度比較低)。

不知道不知道 (Unknown unknown)

或許你也有遇過,你改了某段程式,然後在 QA 時被反應說有地方沒有照規格改;收到反應後你進一步去查,才發現有個本來不知道要改的地方,原來需要改。這種「不知道自己不知道」的狀況,多數時候也意味著軟體的複雜度過高。

以前端工程近年來常有的雙平台語言來說 (例如 React Native 與 Flutter),因為 iOS 與 Android 可能在某些設定上不太一樣,因此如果 API 設計的不好,就會導致踩到意想不到的坑。以我自己寫過的 React Native 來說,用某些 API 時,有時在某些地方會需要去判斷作業系統,在依據作業系統去做特別的調整。這時候若沒有詳細讀文件,很可能就會踩了坑而不自知。然而複雜度低的設計,會是讓使用套件的人,不用去擔心這些細節,不再需要擔心「我會不會漏掉什麼沒注意」。

這邊提供一個我踩過的坑,是 React Native 提供的 LayoutAnimation API,假如在 Android 平台就必須額外加上以下判斷。假如沒有細讀文件,很可能會漏掉。這很容易讓開發者陷入「不知道自己不知道」的問題中。

if (Platform.OS === 'android') {
  if (UIManager.setLayoutAnimationEnabledExperimental) {
    UIManager.setLayoutAnimationEnabledExperimental(true)
  }
}

如何降低軟體設計的複雜度

希望以上有讓大家更了解軟體複雜度相關的概念。讀到這裡相信很自然會問的下個問題是「該如何降低軟體設計時的複雜度呢」。談到降低軟體開發的複雜度,最直覺的方式就是把程式寫得簡單好懂,把可能會出現的極端狀況排除,這樣就能避免自己或別人在未來開發時,不小心踩到過去寫的程式的坑。

除了把程式寫得更簡單一點,另一個更常見的方式,就是透過模組化的方式 (modulization),把程式拆成各個模組,模組之間彼此獨立 (最理想的狀況是完全不影響彼此),這能讓用某個模組的開發者,不用擔心自己做了什麼,導致其他模組炸掉。這種方式又叫模組化設計 (modular design)。

模組化設計是個不小的主題,在下一篇我將統整 《A Philosophy of Software Design》一書談到的,如何透過模組化設計來降低軟體設計複雜度的實際方法。感謝讀到這裡的大家,我們下次見~