有一個健康的自我批評對於專業和個人成長是很重要的。對於程式設計而言,自我批評的意義就是需要能查明設計中、程式碼中、開發中和行為中的無效或反效果的模式。這就是為什麼反面模式對任何程式設計師都很有用的原因。基於我遇到它們的頻率和解決它們花費的時間,本文討論了我發現的反覆出現的、粗略組織的反模式。
某些反模式討論到了它們被普遍認知偏誤的地方,也有的錯誤是直接由它們引起的。這提供了一些關於認知偏誤的文章。維基百科也有一份認知偏誤列表供你參考。
在我們開始前,請記住,教條式的思考阻礙了成長和創新。因此,把下面的列表作為指導而不是一成不變的規則。如果我沒有寫到某些你認為重要的內容,請在下面給我留言!
1 過早優化
97%的時間裡,我們應該忘掉微不足道的效率:過早的優化是萬惡之源。然後,在3%的決定性時刻,我們不應該錯過優化 —— Donald Knuth
不假思索就動手,還不如不做。—— Tim Peters, 《The Zen of Python》
什麼意思?
在你有足夠的資訊能確定在哪優化、如何優化之前,就展開優化。
糟糕的原因
想要知道實踐中的確切瓶頸很困難。試圖在得到實驗資料之前就實行優化,可能會提高程式碼複雜度,並引發難以察舉的bug。
如何避免
把整潔的、可讀性強的、能執行的程式碼放在首位,使用已知的和測試過的演算法和工具。當需要找到瓶頸和決定優化優先順序時,使用分析工具。依賴於測量而不是臆想和推斷。
例子和標誌
在找瓶頸之前做快取。使用複雜的、未經證實的“啟發式”演算法替代出名的、數學上正確的演算法。選擇一種新的、未測試的web框架,當你處於早期階段時,你的伺服器大部分時間處於閒置狀態,那這種框架理論上可以降低高負載下的請求延遲。
棘手的部分
棘手的地方在於知道什麼時候屬於提前優化。提前規劃對於增長而言是很重要的。選擇易於優化和增長的設計和平臺是關鍵。也有可能用“提前優化”作為程式碼糟糕的介面。例如:在有更簡單的、正確的O(n)演算法存在時,卻選擇一個O(n²)的演算法,僅僅因為前者更難理解。
總結
優化之前分析。避免為了效率而犧牲簡潔性,除非效率被驗證了的確是有必要的。
2 單車車庫
“每次我們一討論封面的排版和顏色就會被打斷。討論之後,我們就被要求投票。我認為投給在之前的會議上討論出的顏色是最有效率的,但事實證明我總是少數派!我們最後選擇了紅色。(討論應是藍色)” —— Richard Feynman, 《你在乎其他人的想法嗎》
什麼意思?
花大量時間來辯論和決定瑣碎、太主觀的問題的這種趨勢。
糟糕的原因
這是在浪費時間。Poul-Henning Kamp 在這封郵件裡進行了深入討論。
如何避免
如果你注意到了,那鼓勵團隊成員意識到這種趨勢,並且優先達成決定(投票、拋硬幣等,如果你不得不這樣做的話)。當這個決定有意義時(例如:決定兩種不同的UI設計),考慮隨後A/B的測試來回顧這個決定,而不是進一步的內部討論。
Richard Feynman 不是單車車庫的粉絲
例子和標誌
花費數小時甚至數天來討論你的app要用什麼背景色,或者一個 UI 按鈕應該放在左邊還是右邊,又或者寫程式碼時用製表符縮排而不是空格。
棘手的部分
依我所見,單車車庫相對於提前優化更容易被發現和制止。只要注意你用在做決定和合約上的瑣碎問題的時間,如果有必要,就加以干涉。
總結
避免花費太多時間在瑣碎的事情上。
3 分析癱瘓
只想要預見性,不情願去做簡單有效的事,缺乏清晰的思考,建議混亂……這些構成了歷史上無休止重複的特點。—— Winston Churchill, 《國會辯論》
做也許好過不做。—— Tim Peters, 《The Zen of Python》
什麼意思?
對問題的過度分析,阻礙了行動和進展。
糟糕的原因
過度分析會延緩進展,甚至徹底終止進展。在極端情況下,分析的結果到了要做的時候已經過時了,或者更糟的是,專案或許從來走不出分析階段。當決定難以做出時,很容易想到,更多的資訊將會有助於做出決定——參看 資訊偏誤) 和 效度偏誤。
如何避免
重申一下,意識是有幫助的。重點在於迭代和改進。伴隨著更多有幫助的、有意義的分析得到的資料,每次迭代都會提供更多的反饋。沒有新的資料點,更多的分析將變得越來越讓人猜疑。
例子和標誌
花費數月、甚至數年來決定一個專案的需求、新 UI、或資料庫設計。
棘手的部分
棘手的地方在於要知道什麼時候該從計劃、需求收集和設計階段轉移到實施和測試階段。
總結
寧願迭代,也不要過度分析和猜測。
4 上帝類
簡單勝過複雜。—— Tim Peters,《 The Zen of Python》
什麼意思?
上帝類是控制很多其它類,以及有很多依賴類,也就有更大的責任。
糟糕的原因
上帝類增長到後期就會變成維護人員的地獄——因為它違反了單一責任原則,它們難以單元測試、除錯和記錄文件。
如何避免
通過把責任打散成單一的、清晰的、經過單元測試的、文件易編寫的類,可以避免類變成上帝類。
例子和標誌
尋找類名包含了“manager”、“controller”、“driver”、“system”、或“engine”的類。小心匯入或依賴太多其他類,或操作太多其他類,或有很多處理不相關任務方法的類。
棘手的部分
隨著專案年限、需求和工程師人數的增長,小型的且有著良好意圖的類慢慢地變成了上帝類。重構這些類就變成了浩大的任務。
總結
避免有著太多責任和依賴的龐大的類。
5 新增類恐懼症
稀少勝於繁雜。—— Tim Peters, 《The Zen of Python》
什麼意思?
認為更多的類必然使得設計更加複雜,導致對新增類或把大類分解為一些小類感到恐懼。
糟糕的原因
新增類可以明顯降低複雜度。下面是一張大而亂的毛線團。當解開時,你將得到集團分開的毛線團。類似的,一些簡單的、易於維護、易於記錄文件的類,要遠遠好過於有著太多責任的、單一龐大的、複雜類(參看上面的上帝類的反設計模式)。
如何避免
注意,什麼時候可以簡化設計新增類,以及解耦程式碼中不必要的耦合部分
例子和標誌
考慮下面一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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... |
現在對比下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<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... |
當然,這是一個顯而易見的例子,但是它表明了一點:依賴性強的或有複雜邏輯的大型類,可以分解,也應該被分解為更小的類。結果就是,程式碼將有更多的類,但類更簡單。
棘手的部分
新增類不是魔法彈。通過分解大型類來簡化設計需要深入的考慮分析責任和需求。
總結
類的數量多,並不能說明設計很糟糕。
6 內部平臺效應
那些不理解Unix的人會因為他們的重複發明而遭到譴責。—— Henry Spencer
任何 C 或 Fortran 程式複雜到一定程度之後,都會包含一個臨時開發的、只有一半功能的、不完全符合規格的、到處都是 bug 的、執行速度很慢的 Common Lisp 實現。—— Greenspun 的第十法則
什麼意思?
複雜的軟體系統趨勢在於重實現它們所執行的平臺的特點,或平臺所使用的語言,通常都比較爛。
糟糕的原因
像任務排程和磁碟緩衝區之類平臺級別的任務不太容易做好。糟糕的設計方案容易導致瓶頸和漏洞,特別是系統規模變大以後。重新發明語言中可能已經存在的非正規的語言結構會導致程式碼閱讀起來困難,並且對剛接觸程式碼的人來說,有更陡峭的學習曲線。它還限制了重構和程式碼分析工具的效用。
如何避免
學習使用你的作業系統或平臺所提供的平臺和功能。抵制住建立已有語言結構的誘惑(尤其是因為你不熟悉新語言而找不到你的舊語言的功能)。
例子和標誌
使用你的 MySQL 資料庫做為工作佇列。重實現你自己的磁碟緩衝區機制而不是使用系統的。用 PHP 為你的 web 伺服器編寫計劃任務。用 C 定義 Python 之類的語言結構的巨集。
棘手的部分
在極少情況下,重新實現平臺(JVM、Firefox、Chrome 等)的某些部分可能是有必要的。
總結
避免重新發明你的作業系統或開發平臺已經做得很多的功能。
7 魔法數和字串
明瞭勝於晦澀。—— Tim Peters,《 The Zen of Python》
什麼意思?
使用未命名的數字或字串字面量,而不是在程式碼裡命名為常量。
糟糕的原因
主要問題是由於沒給數字或字串字面量一個描述命名或其他形式的註解,而導致它們的語義被部分或完全的隱藏了。這增加了程式碼理解的難度,並且如果必須要修改常量,尋找和替換或其他的重構工具會導致一些微妙的bug。看看下面的程式碼片段:
1 2 3 |
def create_main_window(): window = Window(600, 600) # etc... |
這兩個數字是什麼?假設第一個是視窗寬度,然後第二個是視窗高度。如果需要修改寬度為800,搜尋和替換就會變得很危險,因為在這個例子中,它也將修改高度的值,或許還有程式碼庫裡其它出現數字 600 的地方。
字串字面量似乎會產生的這類問題不多,但是程式碼裡有未命名的字串字面量,將使得國際化更加困難,並且會導致有著相同字面量卻有著不同語義這種類似的問題。例如,英語中的同義詞可能會造成搜尋和替換的問題;想想看有兩個“point”值出現,其中一個是名詞(比如“she has a point”),另一個是動詞(比如“point out the differences……)。
如何避免
使用命名的常量、資源檢索方法或者註釋。
例子和標誌
上面是一個簡單的例子。這種特定的反面模式非常容易檢測到(除了下面提及的一些棘手的情況。)
棘手的部分
有一個狹窄的灰色地帶,難以確定特定的數字是不是魔術數字。例如,從0開始的索引中的數字0。其他例子還有,用100來計算百分比,用2做奇偶校驗等等。
總結
避免在程式碼中出現未註釋、未命名的數字和字串字面量。
8 數字管理
用程式碼行數來衡量開發進度,無異於用重量來衡量制造飛機的進度。—— Bill Gates
什麼意思?
嚴格地依靠數字來做決定。
糟糕的原因
數字很棒。避免本文提及的兩個反模式(提前優化和單車車庫)的主策略是分析或做A/B測試,來幫助你根據數字優化或做決策,而不是光靠憑空想。然而,盲目的信任數字也很危險。例如,模型無效了但數字還在,或者模型過期了不再能精準的代表現實。這就會導致一些錯誤的決定,尤其是如果它們完全自動化時。請參考自動化偏誤。
依賴於數字做決定(不僅僅是告知)帶來的另一個問題是,策略過程可以隨著時間來調整以達成期望的數字(請參見觀察者期望效應)。分數膨脹就是這種情況的一個例子。HBO 的節目《The Wire/火線》是一個好例子(順便說一句,如果你還沒有看過,你一定要看!),它通過展示警察部門和後來的教育系統用數字遊戲取代了有意義的目標來描述依賴數字的問題。如果你喜歡圖表,下面的圖表展示了 30% 通過率的一場考試的分數分佈,極好地說明了這個觀點。
如何避免
要理智地使用測量和數字,而非盲目。
例子和標誌
使用程式碼行數、提交次數等來評判程式設計師的效率。通過員工呆在公司的小時數來測量他們的貢獻。
棘手的部分
運營規模越大,需要做出決策的數字就越高,這意味著自動化和盲目依賴數字做決策開始蔓延到過程裡了。
總結
用數字來得出你的決策,但不是用數字來做決定。
9 無用的(幽靈)類
要達到完美,不是沒有東西可加,而是沒有東西可減。—— Antoine de Saint Exupéry
什麼意思?
無用類本身沒有真正的責任,經常用來指示呼叫另一個類的方法或增加一層不必要的抽象層。
糟糕的原因
幽靈類增加了測試和維護時候的額外程式碼和複雜度,降低了程式碼可讀性。閱讀者首先需要明白幽靈類在做啥,當然, 通常是什麼都不做,然後鍛鍊自己在心理上替換成實際處理事務的類。
如何避免
不要寫無用的類,或者通過重構來消除。Jack Diederich 有一個很讚的演講,題為《 Stop Writing Classes》,就和這個反模式相關。
例子和標誌
多年前,我正忙於我的碩士學位,當時我是大一 Java 程式設計課的助教。在其中一個實驗課上,我收到了實驗材料,內容是關於使用連結串列來實現棧。我還收到了參考答案。下面是給我的答案,一個 Java 檔案,幾乎沒做改動(限於篇幅我刪除了註釋):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import java.util.EmptyStackException; import java.util.LinkedList; public class LabStack() { private LinkedList<> list; public LabStack() { list = new LinkedList<>(); } 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
,這是又一個可能困惑的地方)。直到今天,我還是無法想象當學生拿到這份實驗材料時,作者會作何感想。當你看到和上例相似的類時,重新考慮一下,它們是否真的需要。
棘手的部分
這裡的建議初看起來和“害怕新增類”的建議相矛盾。重要的是要明白,類在什麼時候發揮著有價值的角色,然後簡化設計,而不是無謂地增加複雜度卻沒有得到益處。
總結
避免沒有真正責任的類。