「保持簡單」這句話是多麼地簡單,但是簡單並不意謂著「輕鬆、容易」,當程式碼夠簡單時表示它的結構透明、清楚、不會隱藏 Bug,我相信大多數的人不會喜歡複雜的東西,程式設計也是如此,當我們一邊罵著前人留下來的 Legacy Code 的時候,自己是否能設計得比現在更為優雅?還是單純地只是想按照著自己的習慣,以自己覺得輕鬆的方式解決問題?
「簡單」指的不是用最輕鬆的方式,抄近路、忽略複雜。
很多時候我們會選擇怎麼簡單怎麼做,但是怎麼簡單怎麼做是一種錯誤的簡單,是用輕鬆的方式抄捷徑的行為,短期內是可以達到效果,讓我們交差了事,但這種做法不會產生簡單的程式,反而是過度簡化、簡單過了頭。
這種簡化過頭的程式是不正確的程式,它會刻意地掩蓋許多不良的設計,每一段簡化過頭的程式都會窩藏著若干個錯誤,而修補被窩藏的錯誤的修補程式就會一個一個被堆積起來,因為複雜已累積到一定程度,這些修補程式也只能選擇抄捷徑、透過 Hacking 的手法修補程式錯誤,如此惡性循環下去,直到程式變成可怕的一團混亂。
簡單設計的特徵
容易使用
容易上手,不需要事先學習太多東西,舉個例子,個人覺得 Google 搜尋功能就是這樣,我可以從最基本的關鍵字搜尋功能開始使用,輸入關鍵字,然後按下搜尋,若需要使用較進階的搜尋條件時,它會逐步地讓我去選擇我應該提供的進階條件值,感覺起來這就是有設計過的。
程式設計也是如此,適度的抽象化將有助於歸納實作的內容,有需要更了解實作的細節時,才再深入展開。
防止誤用
簡單設計的目的,是為了避免被誤用或濫用,但它會增加內部實作的複雜度,以提供一個更簡單的 API。
常見的一種狀況是,資料型態在設計時未明確指定,明明就是數值、日期型態但卻都宣告為 string、整數型態卻用 float 或 double 來表示...etc.,不僅造成混淆,也讓系統曝露在風險之中。
大小很重要
簡單的設計都會被盡可能地做成小得無法再小(非最小,而是有限制的小),盡可能地簡單,小而精確的訊息比較容易消化,就程式設計而言,個人覺得像是精確地命名、降低每個 function 的程式碼行數、封裝程式中具有可變性的部分...etc.,都有助於讓程式碼變得簡單。
較短的程式碼路徑
有一句格言:「任何問題都可以藉由增加一個抽象層來解決」...嗎?
沒有意義的中間層會掩蓋許多複雜的問題,如果我們必須 trace 一連串的 function call 或是穿過許多抽象層來追蹤資料,很快地,我們的精神跟專注力會快速地耗損,甚至想死,這是不人道的,也是沒必要的。
簡單的設計會減少中間層,並確保功能與資料會與需要它的地方緊密相連,也會避免沒必要的繼承、多型、或動態繫結,但盲目地使用就會產生沒必要的複雜性。
穩定性
簡單的設計一定可以被升級及擴充功能,而不需要大量地重寫,如果我們隨著軟體逐漸成熟,而需要不斷地重新修改一部分的程式碼,要嘛,我們每次修改的都是一次性的需求,要嘛,這就是告訴我們,我們的設計還不夠簡單。
一致性會讓程式更清晰
簡單的程式是很容易閱讀的,而且很容易瞭解,個人的喜好與對程式的熟悉度會決定我們用何種風格來排版,但是無論是哪一種風格,請保持一致,因為擁有各式各樣不同樣式、命名方式、設計方法的程式,會造成不必要的混淆。
保持簡單但不愚蠢
當我們遇到 bug 時,通常有兩種方式可以解決它:
- 用最快、輕鬆、容易的方式來解決問題。
- 重構(或重寫)程式,讓它既可修復錯誤,也可保持簡單。
如果我們有把握將程式重構(或重寫)得比原本更簡單,大多時候我們會在心裡認為第 2 個選項是我們要的,但是實際上我們多數會選擇第 1 個選項,因為我們有種種的壓力,我們也都知道選擇第 1 個選項會為我們帶來一些麻煩。
單純地選擇 1 或 2,不是一刀切的問題,我個人有一個簡單的判斷原則,對於有機會建立安全防護網的程式,我會嘗試選擇第 2 個選項,因為只有在安全的狀況之下,我可以無後顧之憂地進行重構(或重寫)、試錯、測試。
設想會減少簡單性
簡單的程式不需要沒必要的假設,無論是關於 domain 的需求、執行環境、後續維護、搭配使用的工具,沒必要的假設會降低簡單性,我們要避免在程式中無意地假設情境。
經常遇到的就是明明程式是我們自己團隊設計的,但是直覺性地我們都會假設參數或回傳值會超出我們的預期,因而下了一些防禦的手段,其實我們只要多溝通,在內部協議出一個合作模式,依據團隊內部所協議的共識下去設計程式,就不需要這些無謂的假設。
但是如果能夠明確地說出我們的假設,例如,這段程式是針對什麼限制或環境而設計的,這種的假設反而可以增加程式的簡單性。
避免過早最佳化
Knuth 的名言:「過早最佳化是所有程式設計邪惡的根源(至少大部分而言)。」,會做最佳化通常是遇到了效能的問題,做最佳化難免會調整原有的演算法,也不可避免地會改變程式碼的外觀,因而讓程式變得比較不簡單。
Caching 就是一個例子,我們為了讓原本的程式可以使用 Cache,必須多加判斷、讓數據一致化,這些都是會增加程式的複雜性,我們在加入 Caching 之前應該嘗試著先去 tune 資料源的效能,很多時候我們會將我們認為很緩慢的部分做最佳化,但是往往瓶頸是在其他地方。
簡單還要剛剛好
簡單的解決方案必須是充分的,不然是無法解決問題的,而「充分」指的是編寫剛剛好解決問題的程式,不要編寫大量我們認為將會用到的程式,不會被用到的程式只是包袱、是額外的負擔、是我們不需要的複雜。
我們有時候會庸人自擾地把還沒發生的需求提前設計或實作,不僅增加了程式的複雜度,而且有可能在未來有很大的機會「會改」,這些都是多的。
沒有人想要寫骯髒、醜陋、複雜的程式,保持簡單需要紀律,只要一次的鬆懈、一次的狗皮藥膏式修補,甚至是一個合理化的藉口,就像破窗效應一樣,紀律被打破之後,總有理由可以不必遵守。
參考資料
- 成為卓越程式設計師的 38 項必修法則 - 第十六章:保持簡單