實戰敏捷開發 Practices of an Agile Developer (4) 程式篇

趁著新年連續假期,終於把這一章念完了:隨著開發的進展,程式逐漸變得怪獸。這章談幾個重點幫助你讓程式容易了解、擴充及維護。

Program Intently and Expressively (寫清楚了解的程式)

厲害的程式是沒有人可以看懂的程式?程式的可閱讀性比寫得方便還重要,寫只會寫一遍,看卻會看很多次。很多時候會有機會要修 Bugs 或是新增功能,這時搞懂本來的程式常常是一開始最困難的地方,如果一開始寫的人就以可讀性為重要目標,那你就會輕鬆的多。

舉個我自己常見的例子就是 Magic Number,很多人喜歡是一個數字來代表 Type code。例如 1 代表 foo、2 代表 bar、3代表…etc,然後程式裡面就直接寫 1, 2, 3。方便是方便,但是沒多久就會忘記這個 1,2,3 各代表什麼意義。最簡單的解決手法就是請改用字串 constant 處理,或者是用 class 來表示 type。

透過程式語言本身的特色可以寫出更有表達力的程式,函數名稱應該傳達出意圖、參數名稱應該表達出他們的用途,寫出好閱讀的程式我們可以避免很多不必要的註解跟說明文件。一些你覺得明顯簡單的地方,不一定對別人也是如此明顯,甚至是幾個月後的你自己。請想想看幾個月後自己來看還會看得懂嗎?

Communicate in Code (寫出可以閱讀的程式)

我們都討厭寫文件的一大理由是:所謂的文件跟程式碼常是分開的兩碼子事,因此很難讓文件跟的上程式的更新,最終變得難以維護。因此最好的寫文件方式,就是透過程式本身和程式中的註解。

註解是用來寫目的、限制條件跟預期結果等,而不是寫出以下這些程式一行行會做些什麼(請不要拿註解再寫一次程式碼在寫的事情)。一個簡單的判斷方式是:寫在函式 “裡面” 的註解多半是不需要的 (尤其是註解寫 what? 而不是註解 why?)。在重構一書更直言回答哪裡需要重構?那就是有註解的地方:如果程式碼前方有一行註釋,就是在提醒你,可以將這段程式碼替換成一個函式,而且可以在註釋的基礎上給這個函式命名。

你要做的事情應該是讓 source code 本身容易了解,而不是透過註解。透過正確的變數命名、好的空白間隔、邏輯分離清楚的執行路徑等手法,我們很少需要在函式裡面註明這些程式在做什麼,我自己的經驗除非是有參考外部的程式碼或複雜演算法,我會多註明參考來源(網址)。否則沒有必要的註解對我來說就像噪音一樣,妨礙閱讀。

變數跟函式的命名是件非常重要的事情,有意義的命名可以傳達出程式怎麼進行,我舉個 Rails 例子:

p = Post.find(param[:id])
if p
   p.destroy
else
   p = Post.new( param[:post] )
   p.save
end


可以改寫成以下這樣具有清楚意含:

existed_post = Post.find(param[:id])
if existed_post
  existed_post.destroy
else
  new_post = Post.new( param[:post] )
  new_post.save
end

有一種 Job security 的方式就是把程式中所有的變數名稱都代換成亂碼,保證拿到的人超級難看得懂。這就是為什麼手工打造的程式碼有其不可取代的重要性,只要拿到整合型 IDE 像是 Dreamweaver 產生出來的 HTML/CSS code 都很想殺人,因為裡面的變數都是編號產生出來的,根本沒辦法閱讀及擴充。

另外就是大家很喜歡用縮寫,這在我之前寫的文章 物件導向程式的九個體操練習 也有提到,這些只有一個字母的暫時變數並沒有辦法傳達出任何可以幫助了解的資訊。

Class, module, method 前面是個寫註解的好地方,而且每個語言都有工具(例如 RDoc, Javadoc… 等)可以幫忙從程式碼中整理出好看的純文件,這些說明通常有:

  • Purpose 目的
  • Requirements(pre-conditions) 預期的輸入是什麼
  • Promises(post-conditions) 什麼樣的輸入,會有什麼樣的預期輸出
  • Exceptions: 有哪些例外(exceptions)情況

這些說明可以幫助我們由大局觀了解程式是怎麼運作的,而不需要仔細看其中每個 method。

Actively Evaluate Trade-Offs (積極地評估 Trade-offs)

效能很重要,但是如果 Performance 已經達到合理要求了,請也考慮實做方不方便、花費的成本跟時間,而不是只想”更快一點”了。天底下沒有完美的解決方案,一切都有 trade-off,如果沒辦法做決定,請去問金主。

這裡的名言警句還是那句老話:Premature optimization is the root of all evil.

Code in Increments (遞增式寫程式)

寫程式的 edit/build/test 週期要短,加上不斷的重構改進,才會寫出容易閱讀、容易測試的小函式和高內聚力的類別,而不是一大沱複雜無法掌握的程式。

Keep It Simple (保持精巧)

當學到新的 Patterns、Principles 或技術時,請抵擋住過度設計和過度複雜化程式碼的壓力。例如在學 GoF 書的時候,很多人常會過度套用其中的樣式,而造成過度設計。盡量發展出最簡明的解決方案,任何笨蛋都有辦法把程式寫的更大更複雜更暴力。

對 Simplicity 概念有興趣的朋友,我推薦可以去翻翻這本書
簡單的法則

Write Cohesive Code (高內聚力的程式)

內聚力一種評估元件裡面的功能相關度高低的概念。如何組織元件 (例如要寫一段新程式,第一件事就是決定要把這段程式放在哪裡) 會造成生產力和維護上的重大的影響。一個元件(component)中的類別應該是要高度相關的、一個類別中的函式應該是要高度相關的。

為什麼內聚力重要?因為我們都希望軟體能夠容易被修改,一旦需要修改,我們希望能夠跳到程式中的某一段,然後只要在那裡改好就可以了。一個內聚力不夠的軟體,當你要修改程式的時候就得多翻很多地方,每遇到某種變化,而要修改的程式碼散佈四處,不但難以找到,也很容易忘記(在重構一書的,指這種壞味道叫做散彈式修改)。

另一個低內聚力的後果是發散式變化:比如說一個類別裡面有五個功能全異的函式,那麼當這五個函式任一個發生需求時,我們就必須要去修改這個類別,而一個頻於修改的不穩定類別會造成維護上的不易。有個物件導向的原則就是在講這件事:Single Responsibility Principle (SRP) 類別變更的原因應僅只有一種 ,換句話說就是一個物件只因一種變化而需要修改。

一個內聚力很糟的例子就是傳統的 PHP/ASP 程式了,每一頁都有 HTML, JavaScript, embedded SQL, business login 全部混在一起,當要變更 Table schema 時,就必須每個頁面都要修改,真是災難一場。解決方法就是引進 MVC 的架構,分離出 presentation logic 和 business logic。

這節的忠告是:一個類別應該要夠聚焦(focused)、一個元件(component)應該要夠小,避免寫出太大的類別或元件、避免寫出功能混雜的通吃類別。

Tell, Don’t Ask

“Procedural code gets information and then makes decisions. Object-oriented code tells objects to do things” by Alec Sharp.

你應該告訴物件你想要什麼(tell),而不是去問(ask)物件的狀態,然後做決定再告訴物件要做什麼。根據被呼叫者的狀態來做決定的邏輯應該是被呼叫者的責任。

一個跟 Tell, Don’t Ask 相關的手法是把所有你的函式都只分成 Command 和 Query 兩種,前者會改變某種狀態(為了方便也許會回傳一些值),後者只是查詢不會有任何更動。Command Query Separation 手法的原意是避免 side-effect,不要同時修改又查詢狀態,以方便測試及除錯。而在這裡透過這個手法也可以同時幫助我們思考 Tell, Don’t Ask。

在 Smalltalk 語言中用 “message passsing” 取代 method calls 一詞,這跟 Tell, Don’t Ask 的感覺類似,像是送訊息而不是執行功能。延伸閱讀可以看看 The Pragmatic Bookshelf 的 Tell, Don’t Ask 一文。

Substitute by Contract (可替換的子型別)

一個保持系統彈性的關鍵,就是將新程式碼替換舊程式碼的時候,其他程式都無須更動。有一個物件導向原則可以幫助我們: Liskov’s Substitution principle (LSP) 子型別必須可以替換他們的父型別。換句話說也就是 本來使用基礎型別(Base class)函式的程式,就算換成衍生類別(Derived class)中也不需修改就可以正常使用。

違反 LSP 的的後果不難想像:假設有個基礎型別 Base 跟他的衍生型別 Derived,而有個函式 f 可以接受 Base 類別的物件當作參數,接著我們把 Derived 轉型成為 Base 之後丟進 f 裡面,如果會爆炸就表示 Derived 違反 LSP 了。這時候 f 的作者為了不要爆炸,所以只好多寫型別判斷是不是 Dervied,於是程式就變髒了。

違反 LSP 原則,你的繼承體系即使仍有重用性,但卻開始失去擴充性,因為類別的用戶必須知道確切的型別才能做操作,當要加入新類別時就要在繼承體系中來回檢查。

另一種違反的形式是衍生類別會丟出基礎類別不會丟出的例外(exception),這時如果基本類別的用戶沒有預期到這種情況,那麼在衍生類別中加入這些例外就會變成不可替換。

使用物件導向的一個常見的錯誤,就是亂用繼承,然後違反 LSP 原則。如果使用繼承,但衍生型別又沒有要可以替換基礎型別來被操作(也就是多型),請想想是不是需要用到繼承?如果 re-use 基礎型別的程式是你使用繼承的原因,那麼你也應該考慮使用 composition 組合關係,讓物件(Called class)擁有並使用另一個物件(Delegate class),也就是把功能委代(delegation)出去。而不是讓 Called Class 去繼承 Base Class。

總之結論就是:is-a 的關係用繼承(inheritance),has-a 或 uses-a 關係用委代(delegation)。

關於 LSP,在敏捷軟體開發一書中有專章比較深入跟嚴謹的討論,像這個 is-a 其實有不少認知上的模糊地帶 :p

參與討論

1 則留言

發佈留言

發表迴響