其它內容

-

軟體設計的哲學 A Philosophy of Software Design 導讀 - 如何降低軟體系統的複雜性?

this.web

身為一位前端開發者,除了基本的 CSS、JS 和框架的應用,如果還想自我提升,增加職涯競爭力,就勢必會接觸到更軟體工程核心的知識內容,今天就來利用這本很有名的書 - 史丹佛教授、Tcl 語言發明者 John Ousterhout 的著作《A Philosophy of Software Design》- 軟體設計的哲學,來帶大家打開軟體工程的大門~

為什麼 app 會越來越難維護?

不管是前端還後端,在軟體開發的過程中,程式碼的複雜性幾乎總是隨時間增長而增長。 Ousterhout 指出,複雜性的成長不僅會使軟體難以理解和維護,還會增加發生錯誤的可能性。

原因在於,隨著系統的擴展,每個新增功能都可能與現有的程式碼相互作用,從而增加了整個系統的複雜性。因此,如何管理和降低這種複雜性就成為了提高軟體品質的關鍵。

整本書的內容就是再討論軟體系統中,如何降低複雜性,我想藉由這個導讀,幫助大家快速裡解本書的核心內容,如果能激起你的興趣而去看這本書就更好了。整篇內容大致如下

  1. 複雜度
  2. 戰術 vs 戰略
  3. 深度模組
  4. 寫好註釋
  5. 投資心態

何謂複雜度

複雜度和系統的結構有關,當你發生以下四種狀況時,就代表程式碼具有一定的複雜度

  1. 可能很難理解一段程式碼是如何運作的。
  2. 可能需要花費很多精力才能實現較小的改進。
  3. 可能不清楚必須修改系統的哪些部分才能進行改進。
  4. 當你解決一個問題時,有時可能會不小心引入新的bug。

簡單說,如果一個軟體系統難以理解和修改,那就很複雜。

而複雜度來自於依賴性(dependencies)和模糊性(obscurity)的累積

在軟體設計裡,依賴性是不可能完全消除的,我們只能減少依賴關係的數量,並使依賴關係盡可能保持簡單和明顯,例如中心全局的狀態管理、自定義 Hooks、和裡的組件拆分等等。

複雜度的第二個原因是模糊性。當重要的訊息不明顯時,就會發生模糊。有很多原因會導致模糊性,例如變數名字沒有很好表示其功能,過於複雜的程式碼或結構、不清楚的註釋或文檔等等。

簡單說,依賴性會讓程式碼變複雜,而模糊性會導致其它開發者接手時,不容易發現應該要注意的地方,所以在開發過程,我們一定要盡可能減少依賴(或保持依賴的簡單)和模糊性。

複雜度的影響

而當複雜度過高時,會帶來以下三個影響:

變更放大:

複雜性的第一個徵兆是,看似簡單的變更需要在許多不同地方進行程式碼修改。

認知負荷:

複雜性的第二個症狀是認知負荷,這是指開發人員需要多少知識才能完成一項任務。

較高的認知負擔意味著開發人員必須花更多的時間來學習所需的信息,並且由於錯過了重要的東西而導致錯誤的風險也更大。

有時,需要更多程式碼的方法實際上更簡單,因為它減少了認知負擔。

未知的未知:

複雜性的第三個症狀是,必須知道修改哪些程式碼才能完成任務,或者開發人員必須獲得哪些資訊才能成功地執行任務。

未知的未知是最糟糕的。一個未知的未知意味著你需要知道一些事情,但是你沒有辦法找到它是什麼,甚至是否有一個問題。直到錯誤出現前,你都不會發現它。

相信大家在寫程式時都有遇過,有些前人寫的 CSS 或 JS 很奇怪,改了之後,網站壞掉才發現為甚麼要這麼寫。這就是未知的未知。

戰術 & 戰略程式設計

在軟體開發裡,戰術和戰略是很常出現的名詞。

戰術是把讓功能正常運作放為重點,例如快速增加新功能或修復錯誤;而戰略則在功能正常的前題下,也保持系統的低複雜性。

書中舉了很好的例子來說明二者的差異:戰術龍捲風

戰術龍捲風是指將戰術程式設計發揮到極致的開發者,他也是一位多產的開發者,寫出程式碼的速度比其他人快得多。在新增功能時,沒有人能比戰術龍捲風更快完成任務。

但是,戰術龍捲風也留下了毀滅的痕跡。通常,其他工程師必須清理戰術龍捲風留下的混亂局面,使得那些工程師的進步似乎比戰術龍捲風慢。

儘管戰略程式設計很重要,但大量的前期投資(例如嘗試設計整個系統)也不是有效的方法 (過於瀑布)。因此,最好的方法是連續進行大量小額投資 。書中建議將總開發時間的 10% 到 20% 用於投資在戰略設計。例如code review、重構程式碼、適時引入設計模式等等。

不過好的戰略,也需要有一定的時間、經驗、技術去支撐,所以自我提升還是非常重要的。

深度模組

在程式設計中,模組是作為一個整體添加的一段程式碼,或是為了易於重複使用而設計的。

而書中認為深模組比淺模組更好,深模組是指能夠提供豐富的功能,同時暴露一個簡單的介面給外部使用的模組。

換句話說,深度模組內部可能非常複雜,但對外部而言,它的使用卻非常簡單。

像是前端 React 最近流行的 Zustand 或是 Vue 的 Pinia,跟如何實現比起來,如何使用他相對簡單的多。

而和深模塊相反的是淺模塊。他的實現並不比接口複雜多少。使用接口而不是直接使用實現的好處不是很大,所以它無助於降低系統的複雜性,例如一個包含大量簡單功能(如字串操作、陣列操作)的函數,但許多函數只是對原生 JavaScript 功能輕微包裝工具庫。

當然我覺得這個部分可能見仁見智,太複雜的深模組可能不太維護或擴充,但太多淺模組又可能讓整個系統變得很複雜。

寫好註釋

一直以來工程師都有寫註釋派和不寫註釋派,此書認為我們應該要寫好註釋,以此降低複雜度。

很多工程師經常用這四個理由,來說服自己不寫註釋,作者有針對每個理由來反駁:

好程式碼本身就是文檔

有些人認為,如果代碼寫得很好,就不需要注釋。當然,在編寫程式時,你可以做一些事情來減少對注釋的需求,比如選擇好的變量名。

但仍然有大有許多是程式碼中無法描述的東西,比如特定設計決策的基本原理,或者調用特定方法的條件等等。且對於大型系統,通過閱讀程式碼來學習行為是不切實際的。

我沒有時間寫註釋

如果要在添加新功能和為現有功能寫注釋之間做出選擇的話,選擇新功能似乎比較合乎邏輯。

但是,軟體項目幾乎總是處於時間壓力之下,並且總會有比寫注釋更優先的事情。所以如果你取消文檔的優先級,則最終將沒有文檔。

註釋過時並產生誤導

注釋有時確實會過時,但這實踐上並不是主要問題。使文檔保持最新狀態並不需要付出巨大的努力。僅當對代碼進行了較大的更改時才需要對文檔進行大的更改,並且代碼更改將比文檔的更改花費更多的時間。(投資心態)

我看到的註釋都無法幫助我更理解 code

這可能是最直接的理由,但這是可以避免的。因為如何寫好註釋也是一門學問,作者提供了幾個有效寫註釋的方法。

如何有效的寫註釋

不要重複程式碼

許多注釋並不是特別有用。最常見的原因是注釋只是單純重複程式碼,例如

// Check if the user is logged in
// 檢查使用者是否登入
if (user.isLoggedIn) {
    // Check for sufficient permissions
    // 檢查是否有足夠的權限
    if (user.hasPermission('viewSensitiveData')) {
        console.log('Access granted. Welcome, ' + user.name);
        // Code to display sensitive information would go here
        // 顯示敏感資訊的程式碼將會放在此處
    } else {
        // Inform them if the user lacks the necessary permissions
        // 如果使用者缺乏必要的權限,請通知他們
        console.log('Access denied. You do not have the required permissions to view this content.');
    }
} else {
    // Prompt the user to log in if they are not already.
    // 如果使用者尚未登入,則提示使用者登入。
    console.log('Please log in to access this content.');
}

以上是一個用戶登陸時的一些基本判斷,事實上,它根本不需要註釋也能清楚知道整段內容,這種註釋就是不必要的。

用變數註釋(低級註釋)提高精度

利用註釋可以讓開發者更清楚知道變數的作用,例如

  • 這個變量的單位是什麼?
  • 邊界條件是包含的還是排除的?
  • 如果允許空值,那它意味著什麼?
  • … 等等需要被注意的地方

當然,檢查使用該變數的程式碼也可以了解這些訊息,但這樣很耗時也容易出錯,所以可以利用註釋來改善。

用高級註釋增強直覺

這些注釋是在比程式邏輯更高的級別上寫的。它們忽略了細節,並幫助開發者理解了程式的整體意圖

高級別的注釋比低級別的注釋更難編寫,因為我們必須以不同的方式或角度來說明程式的作用。我們可以問自己以下三個問題來幫助我們思考:

  1. 這段程式要做什麼?
  2. 你能以何種最簡單方式來解釋程式中的所有內容?
  3. 這段程式最重要的是什麼?

關注 What、Why,而不是 How

專注於「what(做什麼)」和「why(為什麼做)」通常比簡單描述「how(怎麼做)」更有價值。因為程式碼本身應該要清晰表達 how。

並利用 what 幫助開發者快速理解目的功用,並利用 why 說明一些不容易被注意到的地方。

投資心態

Ousterhout 強調,開發者應該持有一種投資心態,意識到今天花時間簡化和重構程式碼,可以在未來節省更多時間和努力。

作者建議花 10 - 20% 的開發時間來重構程式碼和維護註釋,長遠來更這不會更耗成本和時間,,因為在幾個月後就可以開始體驗到這些好處。甚至不久之後,開發速度將比戰術程式設計快至少 10 – 20%。

對於新創公司來說,早期容易感到巨大的壓力,需要盡快發布其早期版本。結果,許多新創公司採取了戰術性的方法,在設計上花費了很少的精力,而在問題出現時則花費了更少的精力進行清理。

但我們應該要意識到,一旦程式碼變成了義大利麵條,幾乎是不可能修復的

而要考慮的另一件事是,公司成功的最重要因素之一就是工程師的品質。降低開發成本的最佳方法是聘請優秀的工程師:他們的成本不會比普通工程師高很多,但生產力高得多。但是,好的工程師對良好的設計感興趣。如果公司程式碼很糟糕,這可能會流失好的工程師。

投資心態鼓勵開發者不斷尋找減少複雜性的機會,即使這意味著短期內需要更多的工作量。但長遠來看,會大大提高軟體的可維護性和可擴展性。

總結

總之,軟體設計的哲學提供了一套全面的方法論,幫助開發者理解並應對軟體開發過程中不斷增長的複雜性。透過實施戰略性設計、深度模組的使用、有效撰寫註釋,以及培養長期的投資心態,我們可以建立更健康、更易於維護和擴展的軟體系統。

後記

其實學前端、寫程式和個人學習、職涯都很類似,程式碼需要花一段時間重構、維護和設計,我們也要定期花一段時間整理、反思與規劃;程式碼需要注意複雜度,我們也需要簡化生活讓注意力集中,從而提升效率、工作品質與生活幸福感。

同時,軟體開發中的投資心態也非常適合個人發展。投入時間和資源來自我提升、學習新技能或深化現有技能,雖然在短期內可能看不到明顯的回報,但從長遠來看,這會極大地豐富我們的生活和職業生涯。

所以軟體工程領域和自我成長一直有很類似的地方,希望大家在工程師這個職位中,除了工作賺錢以外,也能夠藉由這份職業,反思自身的生活,成為更理想的自己。

相關系列文章