每個程序員都應注意的9種反面模式
某種健康的自我批評對于專業和個人成長是至關重要的。對于編程而言,這種自我批評的意義需要檢測出在設計、代碼、過程和行為中的低效和反效果的模式。這就是對反面模式【注1】的理解為什么對于任何程序員都非常有用的原因。本文基于我遇到它們的頻率以及花費多長時間才能消除它們引起的破壞做了反面模式的討論,通過我發現的反復出現的、粗略地組織起來。
討論到的某些反面模式和認知偏誤有些共通的地方,或由它們直接引起的。在我們本文繼續之前,關于認知偏誤的相關鏈接也被提供了。維基百科也有不錯的認知偏誤詞條供你參考【注9】。
在開始之前,我們要切記,教條式思考阻礙了增長和創新,因此把下面的列表做為一套指南、而非一成不變的規則。如果我錯過了你認為重要的東東,請在下面的評論留言!
反面模式的清單包括:
- 不成熟的優化
- 單車車庫【注2】
- 分析癱瘓(Analysis paralysis)
- 上帝類【注3】
- 害怕新增類
- 內部平臺效應(Inner-platform effect)
- 魔術數字和字符串【注4】
- 目標管理【注6】
- 無用的(幽靈)類【注8】
1、不成熟的優化
“在 97% 的時間里,我們應該忘記微不足道的效率:過早的優化是萬惡之源。然而在關鍵的 3% 我們不應該錯過優化的機會。” ——Donald Knuth
“不假思索就動手還不如不做。” ——Tim Peters,《The Zen of Python》【注5】
意思
對于在哪里優化、如何去優化,在你有足夠信息做出有意義的結論之前,就開展的優化。
糟糕的原因
難以確切地知道實踐中的瓶頸。企圖在得到實驗數據之前優化,伴隨著微不足道的改進,有可能增加代碼復雜度和 bug 產生的空間。
如何避免
把編寫整潔、可讀性強的、能運行的代碼擺在首位,使用已知的和測試過的算法和工具。當需要找到瓶頸和優化優先級時,再使用分析工具。依靠策略而非臆測和推斷。
例子和信號
在企圖找到瓶頸之前做緩存。使用復雜的、未經通過的“啟發式”,而不是知名的、數學上正確的算法。選擇一種新的、未經測試的實驗性框架,在理論上可以減少高負載下的請求延遲,當你處于早期階段時,你的服務器大部分時間處于空轉狀態。
棘手的地方
棘 手的地方在于知道什么時候是不成熟的優化。提前規劃對于增長是重要的。選擇易于優化和成長的設計和平臺是關鍵。把“不成熟的優化”做為評判編 寫糟糕代碼的借口也是有可能的。例如:當更簡單的、數學上正確的 O (n) 算法存在時,編寫一個 O (n2) 算法來解決問題,僅僅因為越簡單的算法,越難以理解。
總結
在優化之前做分析。在效率被需要、被觀察到的證據支持之前,避免為了效率而犧牲簡潔。
#p#
2、單車車庫
“我 們總是在討論封面的排版和顏色時出現中斷。每次討論之后,我們被要求投票。我想,對于我們之前在會上決定的相同顏色進行投票,是最有效率的,但 結果顯示,我總是處于少數派!我們最終選擇了紅色。(結果是藍色。)” ——Richard Feynman,《你在乎其他人的想法嗎?》
意思
把過多時間花在瑣碎而且經常是主觀問題的辯論和決定上的趨勢。
糟糕的原因
這是浪費時間。Poul-Henning Kamp 在這封郵件里進行了深入討論。
如何避免
當你注意到這一點時,鼓勵團隊成員注意到這種趨勢,把達成一個決定做為高優先級(投票、拋硬幣等,如果你不得不這樣做的話)。當這個決定有意義(例如,在兩種不同的 UI 設計之間決定)、而不是進一步的內部討論時,考慮稍后的 A/B 測試以重新審視這個決定。
Richard Feynman 不是單車車庫的粉絲
例子和信號
花費數小時或數天討論你的 app 應該使用什么背景色,或者把一個按鈕放在 UI 的左側還是右側,或者在你的代碼庫里的縮進使用制表符而非空格。
棘手的地方
在我看來,單車車庫相較于不成熟的的優化,是更容易發現和防止的。只需要注意到花在做決定和合約上的時間,問題有多么瑣碎,如有必要就加以干預。
總結
不要把過多時間花在瑣碎的事情上。
#p#
3、分析癱瘓
“想要預見性,不愿意行動,而行動是簡單有效的,缺乏清晰的思考,建議混亂……這些構成了歷史上無休止重復的特點。” ——Winston Churchill,國會辯論
“做也許好過不做。” ——Tim Peters,《The Zen of Python》
意思
對問題的過度分析阻礙了行動和進展。
糟糕的原因
過度分析能夠完全延緩或阻止進展。在極端情況下,分析的結果到了實施的時候,會變得毫無用處;或者更糟糕的是,項目或許從來走不出分析階段。當決定難以做出時,更容易臆測出,更多的資訊將有助于做決定 ——參看 資訊偏誤 和 效度偏誤。
如何避免
重申,意識是有幫助的。強調迭代和改善。根據可用于進一步有意義分析的更多的數據點,每次迭代提供更多的反饋。沒有新的數據點,更多的分析將變得越來越讓人猜疑。
例子和信號
花費數月、甚至數年來決定一個項目的需求、新 UI、或數據庫設計。
棘手的地方
棘手的地方在于知道什么時候該從計劃、需求收集和設計階段轉移到實施和測試階段。
總結
寧愿迭代,也不要過度分析和猜測。
#p#
4、上帝類
“簡潔勝過復雜。” ——Tim Peters,《The Zen of Python》
意思
上帝類是控制很多其它類,以及有很多依賴和負責過多的類。
糟糕的原因
上帝類傾向于增長到變成維護噩夢的地步——因為他們違反了單一責任原則,它們難以單元測試、調試和記錄文檔。
如何避免
避免把類變成上帝類,可以通過把責任分解為有著單一化的、清晰定義的、經過單元測試和文檔責任的更小的類。
例子和信號
尋找類名包含了“manager”、“controller”、“driver”、“system”、或“engine”的類。當心 import 或依賴很多其它類、控制太多其它類、或有很多處理不相關任務的方法的類。
上地類知道很多類和/或很多控制。
棘手的地方
隨著項目年限、需求和工程師人數的增長,小型的且有著良好意圖的類慢慢地變成了上帝類。重構這些類就變成了浩大的任務。
總結
避免有著太多責任和依賴的龐大的類。
#p#
5、害怕新增類
“間隔勝于緊湊。” ——Tim Peters,《The Zen of Python》
意思
堅信更多的類必然使得設計更加復雜,從而對新增類或把大類分解為一些小類感到恐懼。
糟糕的原因
新增類可以顯著幫助降低復雜度。貼一副大的雜亂的毛線團。當解開時,你將得到一些分隔開的毛線團。類似地,一些簡單的、易于維護、易于記錄文檔的類,要遠遠好過于有著太多責任的、單一龐大的、復雜類。(參看上面的上帝類的反設計模式)
Photo by absolut_feli on Flickr
如何避免
要注意新增類在什么時候可以簡化設計以及解耦你的代碼中不必要的耦合部分。
例子和信號
考慮下面一個簡單的例子:
class Shape: def __init__(self, shape_type, *args): self.shape_type = shape_type self.args = args def draw (self): if self.shape_type == "circle": center = self.args[0] radius = self.args[1] # Draw a circle... elif self.shape_type == "rectangle": pos = self.args[0] width = self.args[1] height = self.args[2] # Draw rectangle...
現在對比下面的代碼:
class Shape: def draw (self): raise NotImplemented ("Subclasses of Shape should implement method 'draw'.") class Circle (Shape): def __init__(self, center, radius): self.center = center self.radius = radius def draw (self): # Draw a circle... class Rectangle (Shape): def __init__(self, pos, width, height): self.pos = pos self.width = width self.height = height def draw (self): # Draw a rectangle...
當然,這是一個明顯的例子,但是它揭示了一點:內部有著依賴性的或復雜邏輯的大型類,可以、也經常應該被分解為更小的類。最后的代碼將有更多的類,但是更加小型。
棘手的地方
新增類不是一顆神奇的子彈。通過分解大型類來簡化設計,需要對責任和需求進行深入分析。
總結
類的數量多,不一定是糟糕設計的信號。
#p#
6、內部平臺效應
“那些不理解 Unix 的人因對其不良改造而受到譴責。” ——Henry Spencer
“任何 C 或 Fortran 程序復雜到一定程度之后,都會包含一個臨時開發的、只有一半功能的、不完全符合規格的、到處都是 bug 的、運行速度很慢的 Common Lisp 實現。” ——格林斯潘第十法則
意思
復雜軟件系統傾向于它們所運行平臺、或它們所使用編程語言的、功能的重新實現,通常是不良實現。
糟糕的原因
像計劃任務或磁盤緩沖區之類的平臺級別的任務不是容易搞定的。糟糕的設計方案易于帶來瓶頸和 bug,尤其系統規模變大后。重新發明可替代的語言結構來達到語言已有可能的東東,會導致難以閱讀的代碼,對于剛接手代碼庫的人而言,有著更加陡峭的學習 曲線。它還限制了重構和代碼分析工具的效用。
如何避免
學習使用你的操作系統或平臺所提供的平臺和功能。避免創建與已有結構(尤其是因為你不熟悉新語言而找不到你的舊語言的功能)競爭的語言結構的誘惑。
例子和信號
使用你的 MySQL 數據庫做為工作隊列。重新實現你自己的磁盤緩沖區、而不是依賴你的操作系統。用 PHP 為你的 web 服務器編寫計劃任務。用 C 定義 Python 之類的語言結構的宏。
棘手的地方
在極少情況下,重新實現平臺(JVM、Firefox、Chrome 等)的某些部分可能是有必要的。
總結
避免重新發明你的操作系統或開發平臺已經做得很多的功能。
#p#
7、魔術數字和字符串
“明了勝于晦澀。” ——Tim Peters,《The Zen of Python》
意思
直接使用數字或字符串字面量,而不是在代碼里命名的常量。
糟糕的原因
主要問題在于,數字或字符串字面量的語義由于沒有一個解釋型的名字或另一種形式的注解,而被部分或完全地隱藏了。這增加了理解代碼的難度,如果必須要修改常量,那么搜索和替換、或其它重構工具會引入微妙的 bug??紤]下面的代碼片段:
這兩個數字是什么?假定一個數字是窗戶的寬度,第二個是高度。如果需要修改寬度為 800,那么搜索和替換將是危險的,因為在這個例子中,它也將修改高度的值,或許還有代碼庫里其它出現數字 600 的地方。
字符串字面量的這些問題貌似不多,但是代碼里有未命名的字符串字面量,將使得國際化更加困難,對于有著相同字面量卻有著不同語義的情況,就帶來 了類似的問題。比如,英語中的同義詞在搜索和替換時,能夠產生類似問題;假設“point”出現了兩次,一個是指名詞(比如“she has a point”),另一個是動詞(比如“point out the differences……”)。如果一種字符串檢索機制可以明確地指示語義,那么用這種機制替換這樣的字符串字面量,將幫助你區分這兩種情況,當你把這 些字符串送去翻譯時,也就方便多了。
如何避免
使用命名的常量、資源檢索方法、或注釋。
例子和信號
簡單的例子如上所示。這種特定的反面模式非常容易檢測到(期待下面提到的一些棘手的情況)。
棘手的地方
有一個狹窄的灰色地帶,難以區分特定的數字是不是魔術數字。例如,索引從 0 開始的語言中的數字 0。其它例子,用 100 來計算百分比,用 2 做奇偶校驗等。
總結
避免代碼中出現未注解、未命名的數字和字符串字面量。
#p#
8、目標管理
“用代碼行來衡量開發進度,無異于用重量來衡量制造飛機的進度。” ——比爾·蓋茨
意思
嚴格地依靠數字來做決定。
糟糕的原因
數 字是偉大的。避免本文提及的前兩個反面模式(不成熟的優化和單車車庫)的主要策略是分析或 A/B 測試,幫助你根據數字而非臆測來優化或決策。然而,盲目地依賴數字是危險的。比如,數字傾向于比它們有意義的模型要長久,或者模型過期了、不再精確地代表 現實。這會導致錯誤的決策,尤其當它們完全自動化時——參考自動化偏誤。
Do you find yourself commiserating with Pryzbylewski from the HBO show The Wire, Season 4?
依賴數字做決定(不僅僅是告知)的另一個問題是,策略過程可以隨著時間來操作,以達成期望的數字。參看觀察者期望效應【注 7】。分數膨脹就是這種情況的一個例子。HBO 顯示了 The Wire(順便說一句,如果你還沒有看過,你一定要看?。┏錾孛枋隽艘蕾嚁底值膯栴},展現了警察部門和后來的教育系統用數字游戲取代了有意義的目標。如 果你喜歡圖表,下面的圖表展示了 30% 通過率的一場考試的分數分布,極好地說明了這個觀點。
波蘭高中畢業考試中通過率 30% 的分數分布
如何避免
要理智地使用測量和數字,而非盲目。
例子和信號
使用代碼行數、提交次數等來評判程序員的效率。通過員工呆在公司的小時數來測量他們的貢獻。
棘手的地方
運營規模越大,需要做出決策的數字就越高,這意味著自動化和盲目依賴數字做決策開始蔓延到過程里了。
總結
讓數字告知你的決策,而不是決定它們。
#p#
9、無用的(幽靈)類
“達到完美,貌似不是在沒有什么更多的要添加的時候,而是在沒有什么更多的要去掉的時候。” ——Antoine de Saint Exupéry
意思
無用類本身沒有真正的責任,經常用來指示調用另一個類的方法或增加一層不必要的抽象層。
糟糕的原因
幽靈類增加了復雜度、要維護和測試的額外代碼,降低了代碼可讀性——讀者首先需要意識到幽靈類做了什么,它們經常幾乎沒有用處,然后培養自己在精神上用實際處理該責任的類取代幽靈類的使用。
如何避免
不要寫無用類,或者通過重構來消除它們。Jack Diederich 的題為“Stop Writing Classes”就是和這種反面模式相關的。
例子和信號
多年前,我正忙于我的碩士學位,當時是大一 Java 編程課的助教。在其中一個實驗課上,我收到了實驗材料,是關于使用鏈表來實現棧的主題。我還被提供了“答案”的參考。下面是給我的答案,一個 Java 文件,幾乎沒做改動(限于篇幅我刪除了注釋):
import java.util.EmptyStackException; import java.util.LinkedList; public class LabStack<T> { private LinkedList<T> list; public LabStack { list = new LinkedList<T>; } public boolean empty { return list.isEmpty ; } public T peek throws EmptyStackException { if (list.isEmpty ) { throw new EmptyStackException ; } return list.peek ; } public T pop throws EmptyStackException { if (list.isEmpty ) { throw new EmptyStackException ; } return list.pop ; } public void push (T element) { list.push (element); } public int size { return list.size ; } public void makeEmpty { list.clear ; } public String toString { return list.toString ; } }你能想象出我看到這個參考答案的困惑,試圖搞清楚 LabStack
類是做什么的,以及學生應該從這個毫無意義的練習中學到什么。在本例中,這個類的錯誤不是太明顯,它絕對沒有意義!它只是通過實例化的 LinkedList
對象傳遞調用。這個類修改了很多方法的名字(比如把通用的 clear
換成 makeEmpty
),這只會讓用戶困惑。錯誤檢查邏輯完全不必要,因為 LinkedList
里的方法已經做了同樣工作(但是拋出了一個不同的異常,NoSuchElementException
,這是又一個可能困惑的地方)。直到今天,我還是無法想象當學生拿到這份實驗材料時,作者會作何感想。當你看到和上例相似的類時,重新考慮一下,它們是否真的需要。
棘手的地方
這里的建議初看起來和“害怕新增類”的建議相矛盾。重要的是要明白,類在什么時候發揮著有價值的角色和簡化設計,而不是無謂地增加復雜度卻沒有得到益處。
總結
避免沒有真正責任的類。