2019年12月10日

淺談程式碼的可維護性

坊間或教科書上有一些量化的方法可以用來衡量可維護性。不過我想要用比較含糊點的方式來表示這個「可維護性」這個概念。

就一個運行良好且成熟的系統來說,日常的維護並不會有大幅度的變更或修改程式碼的需求,通常只是一些小調整,或是bug的修正。

但是對於還在成長中的系統來說,除了要修復bug以外,還要配合公司、業務發展,大幅度調整現有功能,修改範圍會橫跨多個模組或程式、甚至跨越不同系統,而基於系統現有架構上擴充功能也很常見(調整架構也有機會發生)。

所以基本上,相較於成熟穩定的系統而言,要去「維護」一個成長中的系統是一個比較大的問題,因此在這樣的系統中開發,更要注重「可維護性」,因為工程師們除了要確保舊有功能可以正常運作,還要在現有系統加入或修改功能,還要確保新加入的功能可以正常運作,最棘手的問題是時間壓力

在沒有時間壓力的情況下,一支程式再怎麼難懂、再怎麼複雜,至少時間花下去,多少可以處理掉大部分的問題,但是在時間壓力之下,面對一團亂的程式,怎麼有辦法想出好解法、或真正地找出問題的根本?一套系統沒辦法讓工程師在有限、合理的時間內完成適當地需求,就不具備「可維護性」。

那麼要怎麼提升「可維護性」?

其實提升維護性的方法有很多,但我認為其實不外乎就是想要解決這兩個問題:

  • 我現在這樣寫,不久後我或別人看得懂嗎?
  • 之後要擴充或修改功能的話,容易嗎?

我現在這樣寫,不久後我或別人看得懂嗎

一支程式好不好懂、好不好理解其實因人而異(有些人天資聰穎、或思考方式迥異),但其實大概就是兩個面向,一是命名語意、二是程式流程。

命名語意

先針對命名語意來說,基本上就是要認真命名每個需要命名的地方,變數、參數、類別、函式⋯⋯等。那什麼叫做認真命名?就是要正確地命名,讓命名符合語意、讓看閱讀程式碼的人可以快速理解程式流程,還有程式想要完成的事情。

有人會說,程式看不懂、可以看註解呀。但是註解有兩個問題:

  1. 註解要有人維護,否則會過時。
  2. 長篇大論的註解大家會懶得看。(而且看懂註解還是要再看懂程式一次)

基於以上這兩個問題,我覺得大家在開發的時候,不要想著說「程式寫得不好懂沒關係,把註解寫清楚就好。」還是盡量要做到讓程式碼本身可以說明自己。

舉例來說,以下這段程式,即便沒有註解,也可以看得出來想要做的事情是「學生肚子餓的話就吃蘋果」:

$apple = new Apple();
$student = new Person();
if ($student->isHungry()) {
    $student->eat($apple);
}

但接著這段程式,即便有註解,可能看了一下還會想說,學生是哪個變數?蘋果呢?怎麼吃?是在process什麼?什麼時候needProcess?

// 學生肚子餓的話就吃蘋果
$a = new Apple();
$s = new Person();
if ($s->needProcess()) {
    $s->process($a);
}

我相信在閱讀前者,花的時間絕對比較少,尤其當程式篇幅更大的時候,差距更加明顯,因為人類的大腦記憶及理解範圍是有限的,那麼多不明所以的變數、函式,一但越來越多這樣的程式,通常就是會看完後面,就忘記前面了。

所以認真的命名每個可以命名的地方是很重要的,即便一開始可能會花費較多的時間,去思考用字遣詞,但是讓有意義的命名協助大腦理解程式,會有效地降低大腦的負擔,也會有效地降低後續維護難度。

更多命名建議可參考這篇文章

程式流程

在程式流程這個面向,簡單來說就是:「用對的方式把事情做對」。意思大概是說,除了程式執行結果是對的,怎麼獲得這個結果的流程也要是合理、正確的。

這聽起來是雖然像是廢話,但有時候工程師會因為沒有想好所有可能情況,而誤用了別的方式判斷、或走了不同流程,雖然說當下是正確的,卻其實並不全然是。所以當工程師過一陣子回來檢視或修改程式的時候,會因為忘記了當下的時空背景,而被既有程式上的錯誤流程誤導、而做出錯誤的處理。

比如說,當條件A滿足時,某個函式會回傳true,但是當初的開發者,沒有仔細查證,誤用了條件B作為判斷,可能因為測試案例不夠完整,所以誤打誤撞通過了測試。

然而過了一段時間,這支程式在某個情境下出現問題了,這是當初沒有想到的情境,更糟的是,作者已經離職了,只好另外找工程師來看這支函式,而當他看到條件B時,他完全不知道為什麼當初會使用B做判斷,在心中默默問自己,是不是有什麼特殊考量,又因為已經有很多不同的程式使用了這支函式,他要調整這支程式變得相當困難(到底為什麼要用B判斷?我可以改成A嗎?...)

最後這位可憐的工程師只好直接用強硬、醜陋的手段硬改,加上了莫名的條件C。後續接手這支程式的人,也因為看不懂,開始各種違建,讓整支程式滅目全非⋯⋯

為了要避免這樣的事情反覆發生,一開始還是多花一些時間查證、研究,確保沒有寫錯判斷、或用錯東西吧。換句話說就是,除了注重結果,也要注重流程中的每一步。

之後要擴充或修改功能的話,容易嗎?

擴充或修改的時候,最怕的就是影養到原本舊有程式的運作。因此容易修改、擴充的系統指的是,當我們擴充或修改它的時候,通常不需要去動到舊有的程式碼、或者是即便動到了,我們也很有信心它不會壞掉。

為了要達到這樣的效果,大家最常提到的就是高內聚、低耦合,或是像是SOLID這樣的物件導向開發原則,並搭配各種設計模式(Design Pattern)使用。

這些概念、工具,不外乎就是要讓程式變的容易擴充或修改,讓後續的開發維護可以簡化難度,也讓大型開發團隊彼此間的工作不會互相影響。

所以開發程式的時候,要適當地問自己,這邊這樣寫,如果以後有別的需求,我要怎麼調整?有需要改變目前的程式結構嗎?

但是,沒有人知道未來所有的需求會是如何,不過即便不知道未來的需求是什麼,也不應該一開始就讓程式變成一個完全體,一個完全無法在做任何更動的個體XD

而是在開發的當下,就要盡可能地全面了解目前的開發需求,才開始進行程式攥寫,不要只理解了片面就開始實作,這樣容易犯上前面所提到的「程式流程」問題。

而從另一個角度來看,很多後續的需求,其實還是相當程度是根據現有的程式而延伸的,不太可能完全無關(如果發生的話,快逃吧),因此一開始就長歪的話,後面會越長越歪。

那另外還有一個情況是,有些開發者會喜歡用冷僻或者是高難度的解法,這樣的解法需要高深的技巧、或者要滿足非常多條件,大概就像是只滿足某種數學題型的特殊公式吧。但是這樣的「漂亮」解法,通常是相當沒有彈性的,非常難以調整成符合其他後續需求的樣子,這樣的情況也是值得注意的。

結論

其實說了這麼多,好像也沒提到什麼實質的方法。但換句話來講,我覺得要提升程式的可維護性最重要的是心態,如果開發者對自己寫的程式負責、對自己的團隊成員負責,自然就會想要改善自己的程式品質,進而去尋找改善的方法,這樣的改善,除了可以幫助自己理解程式、也會幫助團隊其他人。

參考

更多關於開發的文章