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