低維護成本的軟體開發

目標

  • 沒有技術債務。
  • 軟體的設計是容易理解的,需要修改軟體時,將很容易找到對應的程式碼。
  • 軟體以可維護為最優先目標,開發者可以在忘記它的數年後回來繼續改進它。
  • 軟體無限接近完成狀態,直到因環境變更而落後或新的需求出現為止都令人滿意。
  • 可以自信地升級主版本相同的依賴項。
  • 當軟體被廢棄時,可順利從現有系統中移除。

原則

謹記「天下沒有白吃的午餐」,你不可能什麼都不做就得到一個低維護成本的軟體專案。實現低維護成本的軟體開發的關鍵不在於少犯幾個錯誤,而在於盡可能把每一件小事都做對,這無疑是相當有難度的。
此處總結了一些有助於實現低維護成本的軟體開發的一般原則:
  • 摒棄所有非必要功能。
  • 停止使用生態環境不健康的語言和工具鏈。
  • 使用具有強表達力的語言,避免在編碼過程中扭曲和偏離語言的常見模式。
  • 停止將巨集用於任何可能產生程式碼壞味道的目的。
  • 只使用具有型別定義或型別標註的語言。
  • 盡可能使用經過時間檢驗的技術,因為這些技術有希望存在更長時間。
  • 全域性地思考問題,避免戰術編程。
  • 積極重構程式碼,允許大量的程式碼行修改以降低軟體熵。
  • 透過將複雜性轉移給使用者來砍掉不那麼必要的設計,因為人性化的設計往往是最難以維護的。現實中類似的例子已經有很多了。讓我們考慮一下Markdown編輯器和所見即所得編輯器:前者需要使用者付出(極少量的)學習成本,而後者幾乎沒有學習成本,然而,後者在可維護性上要比前者差得多:
    • Markdown編輯器更容易編寫出結構清晰、人類可讀的純文字檔案。所見即所得編輯器編寫出的要麼是難以遷移的內部文件模型,要麼是完全陷入混亂的HTML。
    • Markdown文件在2050年還可以使用,Markdown編輯器往往是極簡和可替代的。所見即所得編輯器更容易過時,維持向下相容性更困難,對開發者來說也更難維護,供應商在2050年很可能已經不復存在。
  • 停止使用可能導致供應商鎖定的大型軟體框架。當一個軟體框架正在適配一切環境,或是企圖提供一站式處理解決方案時,它不再是軟體框架,而是一顆定時炸彈。
  • 盡可能減少依賴低品質的軟體套件。這意味著對依賴進行程式碼品質審查,幸運的是,壞程式碼風格很容易識別,往往不需要真正理解軟體套件的運作原理就可以下判斷。
  • 不為沒有向下相容性的平台編寫軟體。
  • 除非軟體功能單一,否則應該盡力避免讓軟體跨平台。
  • 禁止使用雲端運算平台提供的專有服務。
  • 禁止使用不能鎖定子依賴項版本的套件管理器,禁止使用Git儲存庫作為軟體套件的來源。
  • 如非必要,不應迎合現存的標準和協定:
    • 大多數標準和協定是垃圾,其中大部分甚至沒有參考價值,迎合它們只會拖累軟體的發展。例如,你可能會不幸地遇到無法用這些標準和協定實現的需求,然後陷入進退兩難的窘境。
    • 那些非常流行以至於無法被忽視的標準和協定,其流行度大多來自於它們的先發優勢,以及向下相容的保證。這使得它們在生產中的實現充斥著各種奇怪的邊緣案例,收集和應付這些邊緣案例會讓人精疲力竭。
  • 關注工具鏈的抽象水平,停止使用難以遷移或升級的工具鏈。
  • 編寫大量自動化整合測試,以提升依賴項升級時的安全感。
  • 軟體運行所需的時間環境應當是專用的、隔離的,而不是與其他專案共享的。
  • 重視技術的自有化,盡可能剝離會受外部因素影響的依賴。
  • 盡可能縮減核心開發者以外的開發者數量,避免因協作上的摩擦損害專案的開發效率。這件事就像塞車一樣,最好的處理解決方案不是拓寬道路,而是減少同時行車的數量。

重新發明輪子

重新發明輪子是一種反模式,因為總是已經有一個或多個已經發明好的輪子等著你複用。然而,對已有輪子的複用經常引發依賴管理問題:事情起初很順利,但問題很快接踵而至。最簡單的道路往往是最大化技術債務的道路。走在這種道路上,經常會感到挫敗。久而久之,再怎麼有信心和激情的人都會產生習得性無助,認為軟體開發是一件永遠看不到頭的苦差事。
判斷依賴項是否值得被引入是一項很容易被忽視的重要能力,背後需要大量難以言傳的經驗來支持。當你缺乏這項能力時,你會很容易受騙上當,成為未來的受害者乃至加害者的幫凶。由於大多數開發者都缺乏這項能力,行業內劣幣驅逐良幣的現象因此非常普遍。
達到一定規模的開源軟體專案很容易變成屎山:
  • 錯誤的抽象,糟糕的架構,可怕的API表面積。
  • 缺乏令人信服的測試。
  • 文件沒有得到積極維護,與實際不一致,或是掩蓋或是忽略了有用的細節。
  • 太多的戰術編碼,太少的戰略編碼,新增的程式碼量總是比減少的程式碼量要多。
  • 為不存在的需求投入過多的時間成本,導致軟體膨脹和腐爛。
  • ……
除非社群裡有能以戰略視角把關,嚴格執行程式碼審查的權威維護者,否則開源軟體的參與模式天然會製造這種屎山,無論貢獻者的水平有多高。閉源軟體有著同樣的問題,只是閉源軟體的不透明性進一步掩蓋了這些問題,問題被掃到地毯下面不代表問題消失了。
為了從技術債務中解脫,有時你只能戴上NIH帽子。透過重新發明輪子,你可能會注意到,除了能讓我們擺脫糟糕的第三方依賴以外,重新發明輪子有著那些技術債務纏身的開發者們永遠無法知道的好處:
  • 有可能會發明出更好的輪子。
  • 你會越來越擅長發明處理解決方案。
  • 你會越來越有能力辨別依賴項的好壞。
當你以低維護成本為目標的時候,重新發明輪子的收益可能會在未來進一步放大:丟掉債務後,剩下的都是利息,多麼簡單的道理。久而久之,這些正向回饋的疊加會讓重新發明輪子這一選項在你心中佔據越來越重要的位置。不過,在重新發明輪子之前,總是應該先審慎地評估其經濟性。重新發明一切仍然是不可取的,顯然有一些輪子重新發明的性價比很低,並且開發失敗的風險很高,那麼你就得小心。

構建電池式依賴

一些程式語言和框架會聲稱自己附帶電池(batteries included),這指的是它們內建了可以直接使用的高級功能。這些「電池」的本質只是相對穩定的,極力避免暴露底層細節的高級API,正確的抽象和測試使得它們很少出現破壞性變更。
「電池」的概念完全可以套用到依賴管理上,可以建立這樣一種「電池式依賴」:這種依賴是實際使用依賴的專案與依賴項之間的敏感層,它基於依賴項提供一套有序整潔的API,從而允許避免讓業務程式碼直接使用那些野蠻人發明出來的帶有巨型或巨量參數的函數或類別。
由於你只關注這些依賴項中使用到的部分,你可以在中間層編寫自己的測試程式碼,以確保在依賴項版本更新時,其行為仍然符合預期。當未來依賴項失效時,你可以選擇用舊版本的依賴項,更換依賴項,或是自己實現,之前編寫的測試被用來保證重構時的有效性。這便是自己構建的電池和他人構建的電池之間的關鍵區別,你總是可以給自己的電池充電或擴容,而對於他人的電池,通常你只能看著它慢慢沒電。
由於形式上的相似性,可能會混淆電池式依賴和經過封裝的業務邏輯,兩者的主要區別在於它們與依賴項之間的距離。電池式依賴通常距離依賴項很近,而業務邏輯通常距離依賴項很遠。只有在少數場合,例如業務邏輯直接建立在相關依賴上的情況下,兩者才趨於相同。
電池式依賴似乎是處理解決依賴管理問題的聖杯,但仍然有幾個問題:
  • 它畢竟是額外的中間層,當你需要新功能,或是進行破壞性變更時,都要比直接使用依賴項要麻煩些。
  • 建立正確的抽象很難。在摸索正確抽象的道路上,有時不可避免會需要推倒重來。人們對未來的需求也經常缺乏預見性,因此一時正確的抽象也可能在未來顯得不正確。人們害怕專案由於錯誤的抽象陷入反覆修改的困境,所以寧可把自己捲進一個更大的漩渦裡,至少在出錯時怪別人總是更容易,很難反駁這種想法。

使用小型軟體套件

小型軟體套件是Unix哲學和語義化版本控制的共同體現。軟體套件的粒度越小,單個套件的功能就越少,功能越少就越穩定,改變程式碼會打破的東西也就越少。
小型軟體套件有時會帶來一些新的問題,例如軟體套件數量激增帶來的管理問題。這類問題會很棘手,因為它們會隨著日積月累變得越來越難以靠蠻力處理解決。這種趨勢最後會逼出一些專門的處理解決方案,例如Facebook的codemod之類的東西,但使用這些東西仍然很麻煩。

在可讀性和效能之間選擇可讀性

在實際的編碼中,會遇到在可讀性和效能之間搖擺的情況,這時開發者很容易因為一時的短視選擇效能而不是可讀性。選擇效能通常沒有很強烈的理由作為支持,犧牲可讀性換取效能的取捨在日後看來往往並不划算。在一些情況下,開發者選擇效能甚至只是因為「編寫高效能程式碼可以顯得自己很聰明」而產生的幼稚心理收益。
當你在可讀性和效能之間搖擺不定時,應該總是選擇可讀性,這一點再怎麼強調都不為過。非常確定這份程式碼只會被編寫一遍並且這輩子都不可能再改動?選擇可讀性。可讀性更好的程式碼慢1000倍?選擇可讀性。如果真的產生可觀察到的負面效能影響,則再編寫一份程式碼:這份程式碼是高可讀性程式碼經過轉譯後的高效能版本,你需要在旁邊寫上非常詳細的文件來解釋這份程式碼是如何產生的。
一種例外情況是 парни你需要編寫高效能程式,由於你一開始的目標就是高效能,這時可讀性作為一種選項就消失了。不過,即使是高效能程式,也仍然有機會提升程式碼的可讀性,只是這些機會很難被察覺到——往往只有在第二次編寫程式碼的時候,才會注意到。而一旦注意到這種提升可讀性的機會,就沒有理由繼續忽視它們——在維持效能不變的情況下,總是應該盡可能提高可讀性。

欣賞無需維護的軟體

應當將軟體的「無需維護性」視作一種美來欣賞。軟體的「無需維護時間」越長,其「無需維護性」經受的考驗就越久,積累的美就越多。
最關鍵的部分在於,你清楚地知道軟體不是不能維護,也不是難以維護,而是它不需要維護。軟體程式碼就在那裡,但你不需要擺弄它,因為它仍然滿足需要,正常運作,並且運作得很好。
即使到了需要維護的時候,軟體的維護阻力也應當如同窗戶紙那樣,輕輕一捅就可消除。因為沒有技術債務,所以你不需要支付利息。強迫症發作想要升級依賴項?沒問題,因為這項升級可以在十分鐘內完成,而你有很多個十分鐘可以浪費。但比這更好的是你戰勝了自己,及時制止了自己的行為——不僅僅是因為還沒有產生新的需求,還因為你不想破壞軟體隨時間積累出的美。
要培養出這種欣賞能力頗有難度,因為它很反直覺。人們會下意識地將一個事物的新穎程度錯誤地視作一種衡量事物好壞的要素——趕上流行有時似乎比什麼都重要——稍加思考就會發現這並不理性。僅僅在日常生活中,就能發現那些用料紮實,工藝成熟的裝置經常耐用得超乎預料——冰箱這種不間斷工作的家電就是最典型的例子,沒幾個人會每年更換新的冰箱。軟體作為一種數位產物,沒道理會比硬體更快過時,容易更新最不應該成為更新的理由。