<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                ## 示例 +++ get:/url *id=默認值#說明文字 name#說明文字 <<< success { "errNum": 0, "retMsg": "success", "retData": {} } <<< error 這里填寫錯誤的返回碼 以此類推,每個狀態使用 <<< 分割, 第一行添加狀態名稱 +++   程序員修煉之道   The Pragmatic Programmer   作者:Andrew Hunt & David Thomas   譯者:馬維達   目錄 1 我的源碼讓貓給吃了 2 軟件的熵 3 石頭湯與煮青蛙 4 足夠好的軟件 5 你的知識資產 6 交流! 7 重復的危害 8 正交性 9 可撤消性 10 曳光彈 11 原型與便箋 12 領域語言 13 估算 14 純文本的威力 15 shell游戲 16 強力編輯 17 源碼控制 18 調試 19 文本操縱 20 代碼生成器 21 按合約設計(1) 21 按合約設計(2) 22 死程序不說謊 23 斷言式編程 24 何時使用異常 25 怎樣配平資源 1 我的源碼讓貓給吃了   注重實效的程序員的特征是什么?我們覺得是他們處理問題、尋求解決方案時的態度、風格、哲學。他們能夠越出直接的問題去思考,總是設法把問題放在更大的語境中,總是設法注意更大的圖景。畢竟,沒有這樣的更大的語境,你又怎能注重實效?你又怎能做出明智的妥協和有見識的決策?   他們成功的另一關鍵是他們對他們所做的每件事情負責,關于這一點,我們將在“我的源碼讓貓給吃了”中加以討論。因為負責,注重實效的程序員不會坐視他們的項目土崩瓦解。在“軟件的熵”中,我們將告訴你怎樣使你的項目保持整潔。   大多數人發現自己很難接受變化,有時是出于好的理由,有時只是因為固有的惰性。在“石頭湯與煮青蛙”中,我們將考察一種促成變化的策略,并(出于對平衡的興趣)講述一個忽視漸變危險的兩棲動物的警世傳說。   理解你的工作的語境的好處之一是,了解你的軟件必須有多好變得更容易了。有時接近完美是惟一的選擇,但常常會涉及各種權衡。我們將在“足夠好的軟件”中探究這一問題。   當然,你需要擁有廣泛的知識和經驗基礎才能贏得這一切。學習是一個持續不斷的過程。在“你的知識資產”中,我們將討論一些策略,讓你“開足馬力”。   最后,我們沒有人生活在真空中。我們都要花大量時間與他人打交道。在“交流!”中列出了能讓我們更好地做到這一點的幾種途徑。   注重實效的編程源于注重實效的思考的哲學。本章將為這種哲學設立基礎。 在所有弱點中,最大的弱點就是害怕暴露弱點。   ——J. B. Bossuet, Politics from Holy Writ, 1709   依據你的職業發展、你的項目和你每天的工作,為你自己和你的行為負責這樣一種觀念,是注重實效的哲學的一塊基石。注重實效的程序員對他或她自己的職業生涯負責,并且不害怕承認無知或錯誤。這肯定并非是編程最令人愉悅的方面,但它肯定會發生——即使是在最好的項目中。盡管有徹底的測試、良好的文檔以及足夠的自動化,事情還是會出錯。交付晚了,出現了未曾預見到的技術問題。   發生這樣的事情,我們要設法盡可能職業地處理它們。這意味著誠實和坦率。我們可以為我們的能力自豪,但對于我們的缺點——還有我們的無知和我們的錯誤——我們必須誠實。 負責   責任是你主動擔負的東西。你承諾確保某件事情正確完成,但你不一定能直接控制事情的每一個方面。除了盡你所能以外,你必須分析風險是否超出了你的控制。對于不可能做到的事情或是風險太大的事情,你有權不去為之負責。你必須基于你自己的道德準則和判斷來做出決定。   如果你確實同意要為某個結果負責,你就應切實負起責任。當你犯錯誤(就如同我們所有人都會犯錯誤一樣)、或是判斷失誤時,誠實地承認它,并設法給出各種選擇。不要責備別人或別的東西,或是拼湊借口。不要把所有問題都歸咎于供應商、編程語言、管理部門、或是你的同事。也許他(它)們全體或是某幾方在其中扮演了某種角色,但你可以選擇提供解決方案,而非尋找借口。   如果存在供應商不能按時供貨的風險,你應該預先制定一份應急計劃。如果磁盤垮了——帶走了你的所有源碼——而你沒有做備份,那是你的錯。告訴你的老板“我的源碼讓貓給吃了”也無法改變這一點。 提示3: Provide Options, Don’t Make Lame Excuses 提供各種選擇,不要找蹩腳的借口   在你走向任何人、告訴他們為何某事做不到、為何耽擱、為何出問題之前,先停下來,聽一聽你心里的聲音。與你的顯示器上的橡皮鴨交談,或是與貓交談。你的辯解聽起來合理,還是愚蠢?在你老板聽來又是怎樣?   在你的頭腦里把談話預演一遍。其他人可能會說什么?他們是否會問:“你試了這個嗎……”,或是“你沒有考慮那個嗎?”你將怎樣回答?在你去告訴他們壞消息之前,是否還有其他你可以再試一試的辦法?有時,你其實知道他們會說什么,所以還是不要給他們添麻煩吧。   要提供各種選擇,而不是找借口。不要說事情做不到;要說明能夠做什么來挽回局面。必須把代碼扔掉?給他們講授重構的價值(參見重構,184頁)。你要花時間建立原型(prototyping),以確定最好的繼續前進的方式(參見原型與便箋,53頁)?你要引入更好的測試(參見易于測試的代碼,189頁;以及無情的測試,237頁)或自動化(參見無處不在的自動化,230頁),以防止問題再度發生?又或許你需要額外的資源。不要害怕提出要求,也不要害怕承認你需要幫助。   在你大聲說出它們之前,先設法把蹩腳的借口清除出去。如果你必須說,就先對你的貓說。反正,如果小蒂德爾絲(Tiddles,BBC在1969~1974年播出的喜劇節目“Monty Python's Flying Circus”中的著名小母貓——譯注)要承受指責…… 2 軟件的熵   盡管軟件開發幾乎不受任何物理定律的約束,熵(entropy)對我們的影響卻很大。熵是一個來自物理學的概念,指的是某個系統中的“無序”的總量。遺憾的是,熱力學定律保證了宇宙中的熵傾向于最大化。當軟件中的無序增長時,程序員們稱之為“軟件腐爛”(software rot)。   有許多因素可以促生軟件腐爛。其中最重要的一個似乎是開發項目時的心理(或文化)。即使你的團隊只有你一個人,你開發項目時的心理也可能是非常微妙的事情。盡管制定了最好的計劃,擁有最好的開發者,項目在其生命期中仍可能遭遇毀滅和衰敗。而另外有一些項目,盡管遇到巨大的困難和接連而來的挫折,卻成功地擊敗自然的無序傾向,設法取得了相當好的結果。   是什么造成了這樣的差異?   在市區,有些建筑漂亮而整潔,而另一些卻是破敗不堪的“廢棄船只”。為什么?犯罪和城市衰退領域的研究者發現了一種迷人的觸發機制,一種能夠很快將整潔、完整和有人居住的建筑變為破敗的廢棄物的機制[WK82]。   破窗戶。   一扇破窗戶,只要有那么一段時間不修理,就會漸漸給建筑的居民帶來一種廢棄感——一種職權部門不關心這座建筑的感覺。于是又一扇窗戶破了。人們開始亂扔垃圾。出現了亂涂亂畫。嚴重的結構損壞開始了。在相對較短的一段時間里,建筑就被損毀得超出了業主愿意修理的程度,而廢棄感變成了現實。   “破窗戶理論”啟發了紐約和其他大城市的警察部門,他們對一些輕微的案件嚴加處理,以防止大案的發生。這起了作用:管束破窗戶、亂涂亂畫和其他輕微違法事件減少了嚴重罪案的發生。 提示4: Don’t Live with Broken Windows 不要容忍破窗戶   不要留著“破窗戶”(低劣的設計、錯誤決策、或是糟糕的代碼)不修。發現一個就修一個。如果沒有足夠的時間進行適當的修理,就用木板把它釘起來。或許你可以把出問題的代碼放入注釋(comment out),或是顯示“未實現”消息,或是用虛設的數據(dummy data)加以替代。采取某種行動防止進一步的損壞,并說明情勢處在你的控制之下。   我們看到過整潔、運行良好的系統,一旦窗戶開始破裂,就相當迅速地惡化。還有其他一些因素能夠促生軟件腐爛,我們將在別處探討它們,但與其他任何因素相比,置之不理都會更快地加速腐爛的進程。   你也許在想,沒有人有時間到處清理項目的所有碎玻璃。如果你繼續這么想,你就最好計劃找一個大型垃圾罐,或是搬到別處去。不要讓熵贏得勝利。 滅火   作為對照,讓我們講述Andy的一個熟人的故事。他是一個富得讓人討厭的富翁,擁有一所完美、漂亮的房子,里面滿是無價的古董、藝術品,以及諸如此類的東西。有一天,一幅掛毯掛得離他的臥室壁爐太近了一點,著了火。消防人員沖進來救火——和他的房子。但他們拖著粗大、骯臟的消防水管沖到房間門口卻停住了——火在咆哮——他們要在前門和著火處之間鋪上墊子。   他們不想弄臟地毯。   這的確是一個極端的事例,但我們必須以這樣的方式對待軟件。一扇破窗戶——一段設計低劣的代碼、團隊必須在整個項目開發過程中加以忍受的一項糟糕的管理決策——就足以使項目開始衰敗。如果你發現自己在有好些破窗戶的項目里工作,會很容易產生這樣的想法:“這些代碼的其余部分也是垃圾,我只要照著做就行了。”項目在這之前是否一直很好,并沒有什么關系。在最初得出“破窗戶理論”的一項實驗中,一輛廢棄的轎車放了一個星期,無人理睬。而一旦有一扇窗戶被打破,數小時之內車上的設備就被搶奪一空,車也被翻了個底朝天。   按照同樣的道理,如果你發現你所在團隊和項目的代碼十分漂亮——編寫整潔、設計良好,并且很優雅——你就很可能會格外注意不去把它弄臟,就和那些消防員一樣。即使有火在咆哮(最后期限、發布日期、會展演示,等等),你也不會想成為第一個弄臟東西的人。 相關內容: l 石頭湯與煮青蛙,7頁 l 重構,184頁 l 注重實效的團隊,224頁 挑戰: l 通過調查你周邊的計算“環境”,幫助增強你的團隊的能力。選擇兩或三扇“破窗戶”,并與你的同事討論問題何在,以及怎樣修理它們。 l 你能否說出某扇窗戶是何時破的?你的反應是什么?如果它是他人的決策所致,或者是管理部門的指示,你能做些什么? 3 石頭湯與煮青蛙   三個士兵從戰場返回家鄉,在路上餓了。他們看見前面有村莊,就來了精神——他們相信村民會給他們一頓飯吃。但當他們到達那里,卻發現門鎖著,窗戶也關著。經歷了多年戰亂,村民們糧食匱乏,并把他們有的一點糧食藏了起來。   士兵們并未氣餒,他們煮開一鍋水,小心地把三塊石頭放進去。吃驚的村民們走出來望著他們。   “這是石頭湯。”士兵們解釋說。“就放石頭嗎?”村民們問。“一點沒錯——但有人說加一些胡蘿卜味道更好……”一個村民跑開了,又很快帶著他儲藏的一籃胡蘿卜跑回來。   幾分鐘之后,村民們又問:“就是這些了嗎?”   “哦,”士兵們說:“幾個土豆會讓湯更實在。”又一個村民跑開了。   接下來的一小時,士兵們列舉了更多讓湯更鮮美的配料:牛肉、韭菜、鹽,還有香菜。每次都會有一個不同的村民跑回去搜尋自己的私人儲藏品。   最后他們煮出了一大鍋熱氣騰騰的湯。士兵們拿掉石頭,和所有村民一起享用了一頓美餐,這是幾個月以來他們所有人第一次吃飽飯。   在石頭湯的故事里有兩層寓意。士兵戲弄了村民,他們利用村民的好奇,從他們那里弄到了食物。但更重要的是,士兵充當催化劑,把村民團結起來,和他們一起做到了他們自己本來做不到的事情——一項協作的成果。最后每個人都是贏家。   你常常也可以效仿這些士兵。   在有些情況下,你也許確切地知道需要做什么,以及怎樣去做。整個系統就在你的眼前——你知道它是對的。但請求許可去處理整個事情,你會遇到拖延和漠然。大家要設立委員會,預算需要批準,事情會變得復雜化。每個人都會護衛他們自己的資源。有時候,這叫做“啟動雜役”(start-up fatigue)。   這正是拿出石頭的時候。設計出你可以合理要求的東西,好好開發它。一旦完成,就拿給大家看,讓他們大吃一驚。然后說:“要是我們增加……可能就會更好。”假裝那并不重要。坐回椅子上,等著他們開始要你增加你本來就想要的功能。人們發現,參與正在發生的成功要更容易。讓他們瞥見未來,你就能讓他們聚集在你周圍[1]。 提示5: Be a Catalyst for Change 做變化的催化劑 村民的角度   另一方面,石頭湯的故事也是關于溫和而漸進的欺騙的故事。它講述的是過于集中的注意力。村民想著石頭,忘了世界的其余部分。我們都是這樣,每一天。事情會悄悄爬到我們身上。   我們都看見過這樣的癥狀。項目慢慢地、不可改變地完全失去控制。大多數軟件災難都是從微不足道的小事情開始的,大多數項目的拖延都是一天一天發生的。系統一個特性一個特性地偏離其規范,一個又一個的補丁被打到某段代碼上,直到最初的代碼一點沒有留下。常常是小事情的累積破壞了士氣和團隊。 提示6: Remember the Big Picture 記住大圖景   我們沒有做過這個——真的,但有人說,如果你抓一只青蛙放進沸水里,它會一下子跳出來。但是,如果你把青蛙放進冷水里,然后慢慢加熱,青蛙不會注意到溫度的緩慢變化,會呆在鍋里,直到被煮熟。   注意,青蛙的問題與第2節討論的破窗戶問題不同。在破窗戶理論中,人們失去與熵戰斗的意愿,是因為他們覺察到沒有人會在意。而青蛙只是沒有注意到變化。   不要像青蛙一樣。留心大圖景。要持續不斷地觀察周圍發生的事情,而不只是你自己在做的事情。 相關內容: l 軟件的熵,4頁 l 靠巧合編程,172頁 l 重構,184頁 l 需求之坑,202頁 l 注重實效的團隊,224頁 挑戰: l 在評閱本書的草稿時,John Lakos提出這樣一個問題:士兵漸進地欺騙村民,但他們所催生的變化對村民完全有利。但是,漸進地欺騙青蛙,你是在加害于它。當你設法催生變化時,你能否確定你是在做石頭湯還是青蛙湯?決策是主觀的還是客觀的? 4 足夠好的軟件 欲求更好,常把好事變糟。   ——李爾王 1.4   有一個(有點)老的笑話,說一家美國公司向一家日本制造商訂購100 000片集成電路。規格說明中有次品率:10 000片中只能有1片。幾周過后訂貨到了:一個大盒子,里面裝有數千片IC,還有一個小盒子,里面只裝有10片IC。在小盒子上有一個標簽,上面寫著:“這些是次品”。   要是我們真的能這樣控制質量就好了。但現實世界不會讓我們制作出十分完美的產品,特別是不會有無錯的軟件。時間、技術和急躁都在合謀反對我們。   但是,這并不一定就讓人氣餒。如Ed Yourdon發表在IEEE Software上的一篇文章[You95]所描述的,你可以訓練你自己,編寫出足夠好的軟件——對你的用戶、對未來的維護者、對你自己內心的安寧來說足夠好。你會發現,你變得更多產,而你的用戶也會更加高興。你也許還會發現,因為“孵化期”更短,你的程序實際上更好了。   在繼續前進之前,我們需要對我們將要說的話進行限定。短語“足夠好”并非意味著不整潔或制作糟糕的代碼。所有系統都必須滿足其用戶的需求,才能取得成功。我們只是在宣揚,應該給用戶以機會,讓他們參與決定你所制作的東西何時已足夠好。 讓你的用戶參與權衡   通常你是為別人編寫軟件。你常常需要記得從他們那里獲取需求[2]。但你是否常問他們,他們想要他們的軟件有多好?有時候選擇并不存在。如果你的工作對象是心臟起搏器、航天飛機、或是將被廣泛傳播的底層庫,需求就會更苛刻,你的選擇就更有限。但是,如果你的工作對象是全新的產品,你就會有不同的約束。市場人員有需要信守的承諾,最終用戶也許已基于交付時間表制定了各種計劃,而你的公司肯定有現金流方面的約束。無視這些用戶的需求,一味地給程序增加新特性,或是一次又一次潤飾代碼,這不是有職業素養的做法。我們不是在提倡慌張:許諾不可能兌現的時間標度(time scale),為趕上最后期限而削減基本的工程內容,這些同樣不是有職業素養的做法。   你所制作的系統的范圍和質量應該作為系統需求的一部分規定下來。 提示7: Make Quality a Requirements Issue 使質量成為需求問題   你常常會處在須要進行權衡的情形中。讓人驚奇的是,許多用戶寧愿在今天用上有一些“毛邊”的軟件,也不愿等待一年后的多媒體版本。許多預算吃緊的IT部門都會同意這樣的說法。今天的了不起的軟件常常比明天的完美軟件更可取。如果你給用戶某樣東西,讓他們及早使用,他們的反饋常常會把你引向更好的最終解決方案(參見曳光彈,48頁)。 知道何時止步   在某些方面,編程就像是繪畫。你從空白的畫布和某些基本原材料開始,通過知識、藝術和技藝的結合去確定用前者做些什么。你勾畫出全景,繪制背景,然后填入各種細節。你不時后退一步,用批判的眼光觀察你的作品。常常,你會扔掉畫布,重新再來。   但藝術家們會告訴你,如果你不懂得應何時止步,所有的辛苦勞作就會遭到毀壞。如果你一層又一層、細節復細節地疊加,繪畫就會迷失在繪制之中。   不要因為過度修飾和過于求精而毀損完好的程序。繼續前進,讓你的代碼憑著自己的質量站立一會兒。它也許不完美,但不用擔心:它不可能完美(在第6章,171頁,我們將討論在不完美的世界上開發代碼的哲學)。 相關內容: l 曳光彈,48頁 l 需求之坑,202頁 l 注重實效的團隊,224頁 l 極大的期待,255頁 挑戰: l 考察你使用的軟件工具和操作系統的制造商。你能否發現證據,表明這些公司安于發布他們知道不完美的軟件嗎?作為用戶,你是會(1)等著他們清除所有bug,(2)擁有復雜的軟件,并接受某些bug,還是會(3)選擇缺陷較少的更簡單的軟件? l 考慮模塊化對軟件交付的影響。與以模塊化方式設計的系統相比,整體式(monolithic)軟件要達到所需質量,花費的時間更多還是更少?你能找到一個商業案例嗎? 5 你的知識資產 知識上的投資總能得到最好的回報。   ——本杰明·富蘭克林   噢,好樣的老富蘭克林——從不會想不出精練的說教。為什么,如果我們能夠早睡早起,我們就是了不起的程序員——對嗎?早起的鳥兒有蟲吃,但早起的蟲子呢?   然而在這種情況下,Ben確實命中了要害。你的知識和經驗是你最重要的職業財富。   遺憾的是,它們是有時效的資產(expiring asset)。隨著新技術、語言及環境的出現,你的知識會變得過時。不斷變化的市場驅動力也許會使你的經驗變得陳舊或無關緊要。考慮到“網年”飛逝的速度,這樣的事情可能會非常快地發生。   隨著你的知識的價值降低,對你的公司或客戶來說,你的價值也在降低。我們想要阻止這樣的事情,決不讓它發生。 你的知識資產   我們喜歡把程序員所知道的關于計算技術和他們所工作的應用領域的全部事實、以及他們的所有經驗視為他們的知識資產(Knowledge Portfolios)。管理知識資產與管理金融資產非常相似: 1. 嚴肅的投資者定期投資——作為習慣。 2. 多元化是長期成功的關鍵。 3. 聰明的投資者在保守的投資和高風險、高回報的投資之間平衡他們的資產。 4. 投資者設法低買高賣,以獲取最大回報。 5. 應周期性地重新評估和平衡資產。 要在職業生涯中獲得成功,你必須運用同樣的指導方針管理你的知識資產。 經營你的資產 l 定期投資。就像金融投資一樣,你必須定期為你的知識資產投資。即使投資量很小,習慣自身也和總量一樣重要。在下一節中將列出一些示范目標。 l 多元化。你知道的不同的事情越多,你就越有價值。作為底線,你需要知道你目前所用的特定技術的各種特性。但不要就此止步。計算技術的面貌變化很快——今天的熱門技術明天就可能變得近乎無用(或至少是不再搶手)。你掌握的技術越多,你就越能更好地進行調整,趕上變化。 l 管理風險。從高風險、可能有高回報,到低風險、低回報,技術存在于這樣一條譜帶上。把你所有的金錢都投入可能突然崩盤的高風險股票并不是一個好主意;你也不應太保守,錯過可能的機會。不要把你所有的技術雞蛋放在一個籃子里。 l 低買高賣。在新興的技術流行之前學習它可能就和找到被低估的股票一樣困難,但所得到的就和那樣的股票帶來的收益一樣。在Java剛出現時學習它可能有風險,但對于現在已步入該領域的頂尖行列的早期采用者,這樣做得到了非常大的回報。 l 重新評估和平衡。這是一個非常動蕩的行業。你上個月開始研究的熱門技術現在也許已像石頭一樣冰冷。也許你需要重溫你有一陣子沒有使用的數據庫技術。又或許,如果你之前試用過另一種語言,你就會更有可能獲得那個新職位…… 在所有這些指導方針中,最重要的也是最簡單的: 提示8: Invest Regularly in Your Knowledge Portfolio 定期為你的知識資產投資 目標:   關于何時以及增加什么到你的知識資產中,現在你已經擁有了一些指導方針,那么什么是獲得智力資本、從而為你的資產提供資金的最佳方式呢?這里有一些建議。 l 每年至少學習一種新語言。不同語言以不同方式解決相同的問題。通過學習若干不同的方法,可以幫助你拓寬你的思維,并避免墨守成規。此外,現在學習許多語言已容易了許多,感謝可從網上自由獲取的軟件財富(參見267頁)。 l 每季度閱讀一本技術書籍。書店里擺滿了許多書籍,討論與你當前的項目有關的有趣話題。一旦你養成習慣,就一個月讀一本書。在你掌握了你正在使用的技術之后,擴寬范圍,閱讀一些與你的項目無關的書籍。 l 也要閱讀非技術書籍。記住計算機是由人——你在設法滿足其需要的人——使用的,這十分重要。不要忘了等式中人這一邊。 l 上課。在本地的學院或大學、或是將要來臨的下一次會展上尋找有趣的課程。 l 參加本地用戶組織。不要只是去聽講,而要主動參與。與世隔絕對你的職業生涯來說可能是致命的;打聽一下你們公司以外的人都在做什么。 l 試驗不同的環境。如果你只在Windows上工作,就在家玩一玩Unix(可自由獲取的Linux就正好)。如果你只用過makefile和編輯器,就試一試IDE,反之亦然。 l 跟上潮流。訂閱商務雜志和其他期刊(參見262頁的推薦刊物)。選擇所涵蓋的技術與你當前的項目不同的刊物。 l 上網。想要了解某種新語言或其他技術的各種特性?要了解其他人的相關經驗,了解他們使用的特定行話,等等,新聞組是一種很好的方式。上網沖浪,查找論文、商業站點,以及其他任何你可以找到的信息來源。 持續投入十分重要。一旦你熟悉了某種新語言或新技術,繼續前進。學習另一種。   是否在某個項目中使用這些技術,或者是否把它們放入你的簡歷,這并不重要。學習的過程將擴展你的思維,使你向著新的可能性和新的做事方式拓展。思想的“異花授粉”(cross-pollination)十分重要;設法把你學到的東西應用到你當前的項目中。即使你的項目沒有使用該技術,你或許也能借鑒一些想法。例如,熟悉了面向對象,你就會用不同的方式編寫純C程序。 學習的機會   于是你狼吞虎咽地閱讀,在你的領域,你站在了所有突破性進展的前沿(這不是容易的事情)。有人向你請教一個問題,答案是什么?你連最起碼的想法都沒有。你坦白地承認了這一點。   不要就此止步,把找到答案視為對你個人的挑戰。去請教古魯(如果在你們的辦公室里沒有,你應該能在Internet上找到:參見下一頁上的方框)。上網搜索。去圖書館。   如果你自己找不到答案,就去找出能找到答案的人。不要把問題擱在那里。與他人交談可以幫助你建立人際網絡,而因為在這個過程中找到了其他不相關問題的解決方案,你也許還會讓自己大吃一驚。舊有的資產也在不斷增長……   所有閱讀和研究都需要時間,而時間已經很短缺。所以你需要預先規劃。讓自己在空閑的片刻時間里總有東西可讀。花在等醫生上的時間是抓緊閱讀的好機會——但一定要帶上你自己的雜志,否則,你也許會發現自己在翻閱1973年的一篇卷角的關于巴布亞新幾內亞的文章。 批判的思考   最后一個要點是,批判地思考你讀到的和聽到的。你需要確保你的資產中的知識是準確的,并且沒有受到供應商或媒體炒作的影響。警惕聲稱他們的信條提供了惟一答案的狂熱者——那或許適用、或許不適用于你和你的項目。   不要低估商業主義的力量。Web搜索引擎把某個頁面列在最前面,并不意味著那就是最佳選擇;內容供應商可以付錢讓自己排在前面。書店在顯著位置展示某一本書,也并不意味著那就是一本好書,甚至也不說明那是一本受歡迎的書;它們可能是付了錢才放在那里的。 提示9: Critically Analyze What You Read and Hear 批判地分析你讀到的和聽到的   遺憾的是,幾乎再沒有簡單的答案了。但擁有大量知識資產,并把批判的分析應用于你將要閱讀的技術出版物的洪流,你將能夠理解復雜的答案。 與古魯打交道的禮節與教養   隨著Internet在全球普及,古魯們突然變得像你的Enter鍵一樣貼近。那么,你怎樣才能找到一個古魯,怎樣才能找一個古魯和你交談呢?   我們找到了一些簡單的訣竅: l 確切地知道你想要問什么,并盡量明確具體。 l 小心而得體地組織你的問題。記住你是在請求幫助;不要顯得好像是在要求對方回答。 l 組織好問題之后,停下來,再找找答案。選出一些關鍵字,搜索Web。查找適當的FAQ(常見問題的解答列表)。 l 決定你是想公開提問還是私下提問。Usenet新聞組是與專家會面的美妙場所,在那里可以討論幾乎任何問題,但有些人對這些新聞組的公共性質有顧慮。你總是可以用另外的方法:直接發電子郵件給古魯。不管怎樣,要使用有意義的主題(“需要幫助!!!”無益于事)。 l 坐回椅子上,耐心等候。人們很忙,也許需要幾天才能得到明確的答案。   最后,請一定要感謝任何回應你的人。如果你看到有人提出你能夠解答的問題,盡你的一份力,參與解答。 挑戰: l 這周就開始學習一種新語言。總在用C++編程?試試Smalltalk[URL 13]或Squeak[URL 14]。在用Java?試試Eiffel[URL 10]或TOM[URL 15]。關于其他自由編譯器和環境的來源,參見267頁。 l 開始閱讀一本新書(但要先讀完這一本!)。如果你在進行非常詳細的實現和編碼,就閱讀關于設計和架構的書。如果你在進行高級設計,就閱讀關于編碼技術的書。 l 出去和與你的當前項目無關的人、或是其他公司的人談談技術。在你們公司的自助餐廳里結識其他人,或是在本地用戶組織聚會時尋找興趣相投的人。 6 交流! 我相信,被打量比被忽略要好。   ——Mae West, Belle of the Nineties,1934   也許我們可以從West女士那里學到一點什么。問題不只是你有什么,還要看你怎樣包裝它。除非你能夠與他人交流,否則就算你擁有最好的主意、最漂亮的代碼、或是最注重實效的想法,最終也會毫無結果。沒有有效的交流,一個好想法就只是一個無人關心的孤兒。   作為開發者,我們必須在許多層面上進行交流。我們把許多小時花在開會、傾聽和交談上。我們與最終用戶一起工作,設法了解他們的需要。我們編寫代碼,與機器交流我們的意圖;把我們的想法變成文檔,留給以后的開發者。我們撰寫提案和備忘錄,用以申請資源并證明其正當性、報告我們的狀態、以及提出各種新方法。我們每天在團隊中工作,宣揚我們的主意、修正現有的做法、并提出新的做法。我們的時間有很大一部分都花在交流上,所以我們需要把它做好。   我們匯總了我們覺得有用的一些想法。 知道你想要說什么   在工作中使用的更為正式的交流方式中,最困難的部分也許是確切地弄清楚你想要說什么。小說家在開始寫作之前,會詳細地構思情節,而撰寫技術文檔的人卻常常樂于坐到鍵盤前,鍵入“1. 介紹……”,并開始敲入接下來在他們的頭腦里冒出來的任何東西。   規劃你想要說的東西。寫出大綱。然后問你自己:“這是否講清了我要說的所有內容?”提煉它,直到確實如此為止。   這個方法不只適用于撰寫文檔。當你面臨重要會議、或是要與重要客戶通電話時,簡略記下你想要交流的想法,并準備好幾種把它們講清楚的策略。 了解你的聽眾   只有當你是在傳達信息時,你才是在交流。為此,你需要了解你的聽眾的需要、興趣、能力。我們都曾出席過這樣的會議:一個做開發的滑稽人物在發表長篇獨白,講述某種神秘技術的各種優點,把市場部副總裁弄得目光呆滯。這不是交流,而只是空談,讓人厭煩的(annoying)空談。   要在腦海里形成一幅明確的關于你的聽眾的畫面。下一頁的圖1.1中顯示的WISDOM離合詩(acrostic)可能會對你有幫助。   假設你想提議開發一個基于Web的系統,用于讓你們的最終用戶提交bug報告。取決于聽眾的不同,你可以用不同的方式介紹這個系統。如果可以不用在電話上等候,每天24小時提交bug報告,最終用戶將會很高興。你們的市場部門可以利用這一事實促銷。支持部門的經理會因為兩個原因而高興:所需員工更少,問題報告得以自動化。最后,開發者會因為能獲得基于Web的客戶-服務器技術和新數據庫引擎方面的經驗而感到享受。通過針對不同的人進行適當的修正,你將讓他們都為你的項目感到興奮。 選擇時機   這是星期五的下午六點,審計人員進駐已有一周。你的老板最小的孩子在醫院里,外面下著滂沱大雨,這時開車回家肯定是一場噩夢。這大概不是向她提出PC內存升級的好時候。   為了了解你的聽眾需要聽到什么,你需要弄清楚他們的“輕重緩急”是什么。找到一個剛剛因為丟失源碼而遭到老板批評的經理,向她介紹你關于源碼倉庫的構想,你將會擁有一個更容易接納的傾聽者。要讓你所說的適得其時,在內容上切實相關。有時候,只要簡單地問一句“現在我們可以談談……嗎?”就可以了。 圖1.1 WISDOM離合詩——了解聽眾 What do you want them to learn? What is their interest in what you’ve got to say? How sophisticated are they? How much detail do they want? Whom do you want to own the information? How can you motivate them to listen to you? 你想讓他們學到什么? 他們對你講的什么感興趣? 他們有多富有經驗? 他們想要多少細節? 你想要讓誰擁有這些信息? 你如何促使他們聽你說話? 選擇風格   調整你的交流風格,讓其適應你的聽眾。有人要的是正式的“事實”簡報。另一些人喜歡在進入正題之前高談闊論一番。如果是書面文檔,則有人喜歡一大摞報告,而另一些人卻喜歡簡單的備忘錄或電子郵件。如果有疑問,就詢問對方。   但是,要記住,你也是交流事務的一方。如果有人說,他們需要你用一段話進行描述,而你覺得不用若干頁紙就無法做到,如實告訴他們。記住,這樣的反饋也是交流的一種形式。 讓文檔美觀   你的主意很重要。它們應該以美觀的方式傳遞給你的聽眾。   太多程序員(和他們的經理)在制作書面文檔時只關心內容。我們認為這是一個錯誤。任何一個廚師都會告訴你,你可以在廚房里忙碌幾個小時,最后卻會因為飯菜糟糕的外觀而毀掉你的努力。   在今天,已經沒有任何借口制作出外觀糟糕的打印文檔。現代的字處理器(以及像LaTeX和troff這樣的排版系統)能夠生成非常好的輸出。你只需要學習一些基本的命令。如果你的字處理器支持樣式表,就加以利用(你的公司也許已經定義了你可以使用的樣式表)。學習如何設置頁眉和頁腳。查看你的軟件包中包含的樣本文檔,以對樣式和版式有所了解。檢查拼寫,先自動,再手工。畢竟,有一些拼寫錯誤是檢查器找不出來的(After awl, their are spelling miss streaks that the chequer can knot ketch)。 讓聽眾參與   我們常常發現,與制作文檔的過程相比,我們制作出的文檔最后并沒有那么重要。如果可能,讓你的讀者參與文檔的早期草稿的制作。獲取他們的反饋,并汲取他們的智慧。你將建立良好的工作關系,并很可能在此過程中制作出更好的文檔。 做傾聽者   如果你想要大家聽你說話,你必須使用一種方法:聽他們說話。即使你掌握著全部信息,即使那是一個正式會議,你站在20個衣著正式的人面前——如果你不聽他們說話,他們也不會聽你說話。   鼓勵大家通過提問來交談,或是讓他們總結你告訴他們的東西。把會議變成對話,你將能更有效地闡明你的觀點。誰知道呢,你也許還能學到點什么。 回復他人   如果你向別人提問,他們不做出回應,你會覺得他們不禮貌。但當別人給你發送電子郵件或備忘錄、請你提供信息、或是采取某種行動時,你是否經常忘記回復?在匆忙的日常生活中,很容易忘記事情。你應該總是對電子郵件和語音郵件做出回應,即使內容只是“我稍后回復你。”隨時通知別人,會讓他們更容易原諒你偶然的疏忽,并讓他們覺得你沒有忘記他們。 提示10: It’s Both What You Say and the Way You Say It 你說什么和你怎么說同樣重要   除非你生活在真空中,你才不需要能交流。交流越有效,你就越有影響力。 電子郵件交流   我們所說的關于書面交流的所有東西都同樣適用于電子郵件。現在的電子郵件已經發展成為公司內部和公司之間進行交流的主要手段。它被用于討論合約、調解爭端,以及用作法庭證據。但因為某種原因,許多從不會發出低劣的書面文檔的人卻樂于往全世界亂扔外觀糟糕的電子郵件。   我們關于電子郵件的提示很簡單: l 在你按下SEND之前進行校對。 l 檢查拼寫。 l 讓格式保持簡單。有人使用均衡字體(proportional font)閱讀電子郵件,所以你辛苦制作的ASCII藝術圖形在他們看來將像是母雞的腳印一樣亂七八糟。 l 只在你知道對方能夠閱讀rich-text或HTML格式的郵件的情況下使用這些格式。純文本是通用的。 l 設法讓引文減至最少。沒有人喜歡收到一封回郵,其中有100行是他原來的電子郵件,只在最后新添了三個字:“我同意”。 l 如果你引用別人的電子郵件,一定要注明出處。并在正文中進行引用(而不是當做附件)。 l 不要用言語攻擊別人(flame),除非你想讓別人也攻擊你,并老是糾纏你。 l 在發送之前檢查你的收件人名單。最近《華爾街日報》上有一篇文章報道說,有一個雇員通過部門的電子郵件散布對老板的不滿,卻沒有意識到老板也在收件人名單里。 l 將你的電子郵件——你收到的重要文件和你發送的郵件——加以組織并存檔。   如Microsoft和Netscape的好些雇員在1999年司法部調查期間所發現的,e-mail是永久性的。要設法像對待任何書面備忘錄或報告一樣小心對待e-mail。 總結: l 知道你想要說什么。 l 了解你的聽眾。 l 選擇時機。 l 選擇風格。 l 讓文檔美觀。 l 讓聽眾參與。 l 做傾聽者。 l 回復他人。 相關內容: l 原型與便箋,53頁 l 注重實效的團隊,224頁 挑戰: l 有幾本好書討論了開發團隊內部的交流[Bro95, McC95, DL99]。下決心在接下來的18個月里讀完所有這三本書。此外,Dinosaur Brains[Ber96]這本書討論了我們所有人都會帶到工作環境中的“情緒包袱”。 l 在你下一次進行展示、或是撰寫備忘錄支持某種立場時,先試著按第20頁的WISDOM離合詩做一遍。看這樣是否有助于你了解怎樣定位你的講話。如果合適,事后與你的聽眾談一談,看你對他們的需要的估計有多準確。 7 重復的危害 有些提示和訣竅可應用于軟件開發的所有層面,有些想法幾乎是公理,有些過程實際上普遍適用。但是,人們幾乎沒有為這些途徑建立這樣的文檔,你很可能會發現,它們作為零散的段落寫在關于設計、項目管理或編碼的討論中。   在這一章里,我們將要把這些想法和過程集中在一起。頭兩節,“重復的危害”與“正交性”,密切相關。前者提醒你,不要在系統各處對知識進行重復,后者提醒你,不要把任何一項知識分散在多個系統組件中。   隨著變化的步伐加快,我們越來越難以讓應用跟上變化。在“可撤消性”中,我們將考察有助于使你的項目與其不斷變化的環境絕緣的一些技術。   接下來的兩節也是相關的。在“曳光彈”中,我們將討論一種開發方式,能讓你同時搜集需求、測試設計、并實現代碼。這聽起來太好,不可能是真的?的確如此:曳光彈開發并非總是可以應用。“原型與便箋”將告訴你,在曳光彈開發不適用的情況下,怎樣使用原型來測試架構、算法、接口以及各種想法。   隨著計算機科學慢慢成熟,設計者正在制作越來越高級的語言。盡管能夠接受“讓它這樣”(make it so)指令的編譯器還沒有發明出來,在“領域語言”中我們給出了一些適度的建議,你可以自行加以實施。   最后,我們都是在一個時間和資源有限的世界上工作。如果你善于估計出事情需要多長時間完成,你就能更好地在兩者都很匱乏的情況下生存下去(并讓你的老板更高興)。我們將在“估算”中涵蓋這一主題。   在開發過程中牢記這些基本原則,你就將能編寫更快、更好、更強健的代碼。你甚至可以讓這看起來很容易。   給予計算機兩項自相矛盾的知識,是James T. Kirk艦長(出自Star Trek,“星際迷航”——譯注)喜歡用來使四處劫掠的人工智能生命失效的方法。遺憾的是,同樣的原則也能有效地使你的代碼失效。   作為程序員,我們收集、組織、維護和利用知識。我們在規范中記載知識、在運行的代碼中使其活躍起來并將其用于提供測試過程中所需的檢查。   遺憾的是,知識并不穩定。它變化——常常很快。你對需求的理解可能會隨著與客戶的會談而發生變化。政府改變規章制度,有些商業邏輯過時了。測試也許表明所選擇的算法無法工作。所有這些不穩定都意味著我們要把很大一部分時間花在維護上,重新組織和表達我們的系統中的知識。   大多數人都以為維護是在應用發布時開始的,維護就意味著修正bug和增強特性。我們認為這些人錯了。程序員須持續不斷地維護。我們的理解逐日變化。當我們設計或編碼時,出現了新的需求。環境或許變了。不管原因是什么,維護都不是時有時無的活動,而是整個開發過程中的例行事務。   當我們進行維護時,我們必須找到并改變事物的表示——那些嵌在應用中的知識膠囊。問題是,在我們開發的規范、過程和程序中很容易重復表述知識,而當我們這樣做時,我們是在向維護的噩夢發出邀請——在應用發布之前就會開始的噩夢。   我們覺得,可靠地開發軟件、并讓我們的開發更易于理解和維護的惟一途徑,是遵循我們稱之為DRY的原則:   系統中的每一項知識都必須具有單一、無歧義、權威的表示。   我們為何稱其為DRY? 提示11: DRY – Don’t Repeat Yourself 不要重復你自己   與此不同的做法是在兩個或更多地方表達同一事物。如果你改變其中一處,你必須記得改變其他各處。或者,就像那些異形計算機,你的程序將因為自相矛盾而被迫屈服。這不是你是否能記住的問題,而是你何時忘記的問題。   你會發現DRY原則在全書中一再出現,并且常常出現在與編碼無關的語境中。我們覺得,這是注重實效的程序員的工具箱里最重要的工具之一。   在這一節我們將概述重復的問題,并提出對此加以處理的一般策略。 重復是怎樣發生的   我們所見到的大多數重復都可歸入下列范疇: l 強加的重復(imposed duplication)。開發者覺得他們無可選擇——環境似乎要求重復。 l 無意的重復(inadvertent duplication)。開發者沒有意識到他們在重復信息。 l 無耐性的重復(impatient duplication)。開發者偷懶,他們重復,因為那樣似乎更容易。 l 開發者之間的重復(interdeveloper duplication)。同一團隊(或不同團隊)的幾個人重復了同樣的信息。 讓我們更詳細地看一看這四個以“i ”開頭的重復。 強加的重復   有時,重復似乎是強加給我們的。項目標準可能要求建立含有重復信息的文檔,或是重復代碼中的信息的文檔。多個目標平臺各自需要自己的編程語言、庫以及開發環境,這會使我們重復共有的定義和過程。編程語言自身要求某些重復信息的結構。我們都在我們覺得無力避免重復的情形下工作過。然而也有一些方法,可用于把一項知識存放在一處,以遵守DRY原則,同時也讓我們的生活更容易一點。這里有一些這樣的技術: 信息的多種表示。在編碼一級,我們常常需要以不同的形式表示同一信息。我們也許在編寫客戶-服務器應用,在客戶和服務器端使用了不同的語言,并且需要在兩端都表示某種共有的結構。我們或許需要一個類,其屬性是某個數據庫表的schema(模型、方案)的鏡像。你也許在撰寫一本書,其中包括的程序片段,也正是你要編譯并測試的程序。   發揮一點聰明才智,你通常能夠消除重復的需要。答案常常是編寫簡單的過濾器或代碼生成器。可以在每次構建(build)軟件時,使用簡單的代碼生成器,根據公共的元數據表示構建多種語言下的結構(示例參見圖3.4,106頁)。可以根據在線數據庫schema、或是最初用于構建schema的元數據,自動生成類定義。本書中摘錄的代碼,由預處理器在我們每次對文本進行格式化時插入。訣竅是讓該過程成為主動的,這不能是一次性轉換,否則我們就會退回到重復數據的情況。 代碼中的文檔。程序員被教導說,要給代碼加上注釋:好代碼有許多注釋。遺憾的是,沒有人教給他們,代碼為什么需要注釋:糟糕的代碼才需要許多注釋。   DRY法則告訴我們,要把低級的知識放在代碼中,它屬于那里;把注釋保留給其他的高級說明。否則,我們就是在重復知識,而每一次改變都意味著既要改變代碼,也要改變注釋。注釋將不可避免地變得過時,而不可信任的注釋比完全沒有注釋更糟(關于注釋的更多信息,參見全都是寫,248頁)。 文檔與代碼。你撰寫文檔,然后編寫代碼。有些東西變了,你修訂文檔、更新代碼。文檔和代碼都含有同一知識的表示。而我們都知道,在最緊張的時候——最后期限在逼近,重要的客戶在喊叫——我們往往會推遲文檔的更新。   Dave曾經參與過一個國際電報交換機項目的開發。很容易理解,客戶要求提供詳盡的測試規范,并要求軟件在每次交付時都通過所有測試。為了確保測試準確地反映規范,開發團隊用程序方式、根據文檔本身生成這些測試。當客戶修訂他們的規范時,測試套件會自動改變。有一次團隊向客戶證明了,該過程很健全,生成驗收測試在典型情況下只需要幾秒種。 語言問題。許多語言會在源碼中強加可觀的重復。如果語言使模塊的接口與其實現分離,就常常會出現這樣的情況。C與C++有頭文件,在其中重復了被導出變量、函數和(C++的)類的名稱和類型信息。Object Pascal甚至會在同一文件里重復這些信息。如果你使用遠地過程調用或CORBA[URL 29],你將會在接口規范與實現它的代碼之間重復接口信息。   沒有什么簡單的技術可用于克服語言的這些需求。盡管有些開發環境通過自動生成頭文件、隱藏了對頭文件的需要,而Object Pascal允許你縮寫重復的函數聲明,你通常仍受制于給予你的東西。至少對于大多數與語言有關的問題,與實現不一致的頭文件將會產生某種形式的編譯或鏈接錯誤。你仍會弄錯事情,但至少,你將在很早的時候就得到通知。   再思考一下頭文件和實現文件中的注釋。絕對沒有理由在這兩種文件之間重復函數或類頭注釋(header comment)。應該用頭文件記載接口問題,用實現文件記載代碼的使用者無須了解的實際細節。 無意的重復   有時,重復來自設計中的錯誤。   讓我們看一個來自配送行業的例子。假定我們的分析揭示,一輛卡車有車型、牌照號、司機及其他一些屬性。與此類似,發運路線的屬性包括路線、卡車和司機。基于這一理解,我們編寫了一些類。   但如果Sally打電話請病假、我們必須改換司機,事情又會怎樣呢?Truck和DeliverRoute都包含有司機。我們改變哪一個?顯然這樣的重復很糟糕。根據底層的商業模型對其進行規范化(normalize)——卡車的底層屬性集真的應包含司機?路線呢?又或許我們需要第三種對象,把司機、卡車及路線結合在一起。不管最終的解決方案是什么,我們都應避免這種不規范的數據。   當我們擁有多個互相依賴的數據元素時,會出現一種不那么顯而易見的不規范數據。讓我們看一個表示線段的類: class Line { public: Point start; Point end; double length; };   第一眼看上去,這個類似乎是合理的。線段顯然有起點和終點,并總是有長度(即使長度為零)。但這里有重復。長度是由起點和終點決定的:改變其中一個,長度就會變化。最好是讓長度成為計算字段: class Line { public: Point start; Point end; double length() { return stanceTo(end); } };   在以后的開發過程中,你可以因為性能原因而選擇違反DRY原則。這經常會發生在你需要緩存數據,以避免重復昂貴的操作時。其訣竅是使影響局部化。對DRY原則的違反沒有暴露給外界:只有類中的方法需要注意“保持行為良好”。 class Line { private: bool changed; double length; Point start; Point end; public: void setStart(Point p) { start = p; changed = true; } void setEnd(Point p) { end = p; changed = true; } Point getStart(void) { return start; } Point getEnd(void) { return end; } double getLength() { if (changed) { length = stanceTo(end); changed = false; } return length; } };   這個例子還說明了像Java和C++這樣的面向對象語言的一個重要問題。在可能的情況下,應該總是用訪問器(accessor)函數讀寫對象的屬性。這將使未來增加功能(比如緩存)變得更容易。 無耐性的重復   每個項目都有時間壓力——這是能夠驅使我們中間最優秀的人走捷徑的力量。需要與你寫過的一個例程相似的例程?你會受到誘惑,去拷貝原來的代碼,并做出一些改動。需要一個表示最大點數的值?如果我改動頭文件,整個項目就得重新構建。也許我應該在這里使用直接的數字(literal number),這里,還有這里,需要一個與Java runtime中的某個類相似的類?源碼在那里(你有使用許可),那么為什么不拷貝它、并做出你所需的改動呢?   如果你覺得受到誘惑,想一想古老的格言:“欲速則不達”。你現在也許可以節省幾秒鐘,但以后卻可能損失幾小時。想一想圍繞著Y2K慘敗的種種問題。其中許多問題是由開發者的懶惰造成的:他們沒有參數化日期字段的尺寸,或是實現集中的日期服務庫。   無耐性的重復是一種容易檢測和處理的重復形式,但那需要你接受訓練,并愿意為避免以后的痛苦而預先花一些時間。 開發者之間的重復   另一方面,或許是最難檢測和處理的重復發生在項目的不同開發者之間。整個功能集都可能在無意中被重復,而這些重復可能幾年里都不會被發現,從而導致各種維護問題。我們親耳聽說過,美國某個州在對政府的計算機系統進行Y2K問題檢查時,審計者發現有超出10,000個程序,每一個都有自己的社會保障號驗證代碼。   在高層,可以通過清晰的設計、強有力的技術項目領導(參見288頁“注重實效的團隊”一節中的內容)、以及在設計中進行得到了充分理解的責任劃分,對這個問題加以處理。但是,在模塊層,問題更加隱蔽。不能劃入某個明顯的責任區域的常用功能和數據可能會被實現許多次。   我們覺得,處理這個問題的最佳方式是鼓勵開發者相互進行主動的交流。設置論壇,用以討論常見問題(在過去的一些項目中,我們設置了私有的Usenet新聞組,用于讓開發者交換意見,進行提問。這提供了一種不受打擾的交流方式——甚至跨越多個站點——同時又保留了所有言論的永久歷史)。讓某個團隊成員擔任項目資料管理員,其工作是促進知識的交流。在源碼樹中指定一個中央區域,用于存放實用例程和腳本。一定要閱讀他人的源碼與文檔,不管是非正式的,還是進行代碼復查。你不是在窺探——你是在向他們學習。而且要記住,訪問是互惠的——不要因為別人鉆研(亂鉆?)你的代碼而苦惱。 提示12: Make It Easy to Reuse 讓復用變得容易   你所要做的是營造一種環境,在其中要找到并復用已有的東西,比自己編寫更容易。如果不容易,大家就不會去復用。而如果不進行復用,你們就會有重復知識的風險。 相關內容: l 正交性,34頁 l 文本操縱,99頁 l 代碼生成器,102頁 l 重構,184頁 l 注重實效的團隊,224頁 l 無處不在的自動化,230頁 l 全都是寫,248頁 8 正交性   如果你想要制作易于設計、構建、測試及擴展的系統,正交性是一個十分關鍵的概念,但是,正交性的概念很少被直接講授,而常常是你學習的各種其他方法和技術的隱含特性。這是一個錯誤。一旦你學會了直接應用正交性原則,你將發現,你制作的系統的質量立刻就得到了提高。 什么是正交性   “正交性”是從幾何學中借來的術語。如果兩條直線相交成直角,它們就是正交的,比如圖中的坐標軸。用向量術語說,這兩條直線互不依賴。沿著某一條直線移動,你投影到另一條直線上的位置不變。   在計算技術中,該術語用于表示某種不相依賴性或是解耦性。如果兩個或更多事物中的一個發生變化,不會影響其他事物,這些事物就是正交的。在設計良好的系統中,數據庫代碼與用戶界面是正交的:你可以改動界面,而不影響數據庫;更換數據庫,而不用改動界面。   在我們考察正交系統的好處之前,讓我們先看一看非正交系統。 非正交系統   你正乘坐直升機游覽科羅拉多大峽谷,駕駛員——他顯然犯了一個錯誤,在吃魚,他的午餐——突然呻吟起來,暈了過去。幸運的是,他把你留在了離地面100英尺的地方。你推斷,升降桿控制總升力,所以輕輕將其壓低可以讓直升機平緩降向地面。然而,當你這樣做時,卻發現生活并非那么簡單。直升機的鼻子向下,開始向左盤旋下降。突然間你發現,你駕駛的這個系統,所有的控制輸入都有次級效應。壓低左手的操作桿,你需要補償性地向后移動右手柄,并踩右踏板。但這些改變中的每一項都會再次影響所有其他的控制。突然間,你在用一個讓人難以置信的復雜系統玩雜耍,其中每一項改變都會影響所有其他的輸入。你的工作負擔異常巨大:你的手腳在不停地移動,試圖平衡所有交互影響的力量。   直升機的各個控制器斷然不是正交的。 正交的好處   如直升機的例子所闡明的,非正交系統的改變與控制更復雜是其固有的性質。當任何系統的各組件互相高度依賴時,就不再有局部修正(local fix)這樣的事情。 提示13: Eliminate Effects Between Unrelated Things 消除無關事物之間的影響   我們想要設計自足(self-contained)的組件:獨立,具有單一、良好定義的目的(Yourdon和Constantine稱之為內聚(cohesion)[YC86])。如果組件是相互隔離的,你就知道你能夠改變其中之一,而不用擔心其余組件。只要你不改變組件的外部接口,你就可以放心:你不會造成波及整個系統的問題。   如果你編寫正交的系統,你得到兩個主要好處:提高生產率與降低風險。 提高生產率: l 改動得以局部化,所以開發時間和測試時間得以降低。與編寫單個的大塊代碼相比,編寫多個相對較小的、自足的組件更為容易。你可以設計、編寫簡單的組件,對其進行單元測試,然后把它們忘掉——當你增加新代碼時,無須不斷改動已有的代碼。 l 正交的途徑還能夠促進復用。如果組件具有明確而具體的、良好定義的責任,就可以用其最初的實現者未曾想象過的方式,把它們與新組件組合在一起。 l 如果你對正交的組件進行組合,生產率會有相當微妙的提高。假定某個組件做M件事情,而另一個組件做N件事情。如果它們是正交的,而你把它們組合在一起,結果就能做M x N件事情。但是,如果這兩個組件是非正交的,它們就會重疊,結果能做的事情就更少。通過組合正交的組件,你的每一份努力都能得到更多的功能。 降低風險   正交的途徑能降低任何開發中固有的風險: l 有問題的代碼區域被隔離開來。如果某個模塊有毛病,它不大可能把病癥擴散到系統的其余部分。要把它切掉,換成健康的新模塊也更容易。 l 所得系統更健壯。對特定區域做出小的改動與修正,你所導致的任何問題都將局限在該區域中。 l 正交系統很可能能得到更好的測試,因為設計測試、并針對其組件運行測試更容易。 l 你不會與特定的供應商、產品、或是平臺緊綁在一起,因為與這些第三方組件的接口將被隔離在全部開發的較小部分中。 讓我們看一看在工作中應用正交原則的幾種方式。 項目團隊   你是否注意到,有些項目團隊很有效率,每個人都知道要做什么,并全力做出貢獻,而另一些團隊的成員卻老是在爭吵,而且好像無法避免互相妨礙?   這常常是一個正交性問題。如果團隊的組織有許多重疊,各個成員就會對責任感到困惑。每一次改動都需要整個團隊開一次會,因為他們中的任何一個人都可能受到影響。   怎樣把團隊劃分為責任得到了良好定義的小組,并使重疊降至最低呢?沒有簡單的答案。這部分地取決于項目本身,以及你對可能變動的區域的分析。這還取決于你可以得到的人員。我們的偏好是從使基礎設施與應用分離開始。每個主要的基礎設施組件(數據庫、通信接口、中間件層,等等)有自己的子團隊。如果應用功能的劃分顯而易見,那就照此劃分。然后我們考察我們現有的(或計劃有的)人員,并對分組進行相應的調整。   你可以對項目團隊的正交性進行非正式的衡量。只要看一看,在討論每個所需改動時需要涉及多少人。人數越多,團隊的正交性就越差。顯然,正交的團隊效率也更高(盡管如此,我們也鼓勵子團隊不斷地相互交流)。 設計   大多數開發者都熟知需要設計正交的系統,盡管他們可能會使用像模塊化、基于組件、或是分層這樣的術語描述該過程。系統應該由一組相互協作的模塊組成,每個模塊都實現不依賴于其他模塊的功能。有時,這些組件被組織為多個層次,每層提供一級抽象。這種分層的途徑是設計正交系統的強大方式。因為每層都只使用在其下面的層次提供的抽象,在改動底層實現、而又不影響其他代碼方面,你擁有極大的靈活性。分層也降低了模塊間依賴關系失控的風險。你將常常看到像下一頁的圖2.1這樣的圖表示的層次關系。   對于正交設計,有一種簡單的測試方法。一旦設計好組件,問問你自己:如果我顯著地改變某個特定功能背后的需求,有多少模塊會受影響?在正交系統中,答案應該是“一個”。移動GUI面板上的按鈕,不應該要求改動數據庫schema。增加語境敏感的幫助,也不應該改動記賬子系統。 圖2.1 典型的層次圖   讓我們考慮一個用于監視和控制供暖設備的復雜系統。原來的需求要求提供圖形用戶界面,但后來需求被改為要增加語音應答系統,用按鍵電話控制設備。在正交地設計的系統中,你只需要改變那些與用戶界面有關聯的模塊,讓它們對此加以處理:控制設備的底層邏輯保持不變。事實上,如果你仔細設計你的系統結構,你應該能夠用同一個底層代碼庫支持這兩種界面。157頁的“它只是視圖”將討論怎樣使用模型-視圖-控制器(MVC)范型編寫解耦的代碼,該范型在這里的情況下也能很好地工作。   還要問問你自己,你的設計在多大程度上解除了與現實世界中的的變化的耦合?你在把電話號碼當作顧客標識符嗎?如果電話公司重新分配了區號,會怎么樣?不要依賴你無法控制的事物屬性。 8 正交性(2) 工具箱與庫   在你引入第三方工具箱和庫時,要注意保持系統的正交性。要明智地選擇技術。   我們曾經參加過一個項目,在其中需要一段Java代碼,既運行在本地的服務器機器上,又運行在遠地的客戶機器上。要把類按這樣的方式分布,可以選用RMI或CORBA。如果用RMI實現類的遠地訪問,對類中的遠地方法的每一次調用都可能會拋出異常;這意味著,一個幼稚的實現可能會要求我們,無論何時使用遠地類,都要對異常進行處理。在這里,使用RMI顯然不是正交的:調用遠地類的代碼應該不用知道這些類的位置。另一種方法——使用CORBA——就沒有施加這樣的限制:我們可以編寫不知道我們類的位置的代碼。   在引入某個工具箱時(甚或是來自你們團隊其他成員的庫),問問你自己,它是否會迫使你對代碼進行不必要的改動。如果對象持久模型(object persistence scheme)是透明的,那么它就是正交的。如果它要求你以一種特殊的方式創建或訪問對象,那么它就不是正交的。讓這樣的細節與代碼隔離具有額外的好處:它使得你在以后更容易更換供應商。   Enterprise Java Beans(EJB)系統是正交性的一個有趣例子。在大多數面向事務的系統中,應用代碼必須描述每個事務的開始與結束。在EJB中,該信息是作為元數據,在任何代碼之外,以聲明的方式表示的。同一應用代碼不用修改,就可以運行在不同的EJB事務環境中。這很可能是將來許多環境的模型。   正交性的另一個有趣的變體是面向方面編程(Aspect-Oriented Programming,AOP),這是Xerox Parc的一個研究項目([KLM+97]與[URL 49])。AOP讓你在一個地方表達本來會分散在源碼各處的某種行為。例如,日志消息通常是在源碼各處、通過顯式地調用某個日志函數生成的。通過AOP,你把日志功能正交地實現到要進行日志記錄的代碼中。使用AOP的Java版本,你可以通過編寫aspect、在進入類Fred的任何方法時寫日志消息: aspect Trace { advise * Fred.*(..) { static before { Lite("-> Entering " + thisJoinPthodName); } } }   如果你把這個方面編織(weave)進你的代碼,就會生成追蹤消息。否則,你就不會看到任何消息。不管怎樣,你原來的源碼都沒有變化。 編碼   每次你編寫代碼,都有降低應用正交性的風險。除非你不僅時刻監視你正在做的事情,也時刻監視應用的更大語境,否則,你就有可能無意中重復其他模塊的功能,或是兩次表示已有的知識。   你可以將若干技術用于維持正交性: l 讓你的代碼保持解耦。編寫“羞怯”的代碼——也就是不會沒有必要地向其他模塊暴露任何事情、也不依賴其他模塊的實現的模塊。試一試我們將在183頁的“解耦與得墨忒耳法則”中討論的得墨忒耳法則(Law of Demeter)[LH89]。如果你需要改變對象的狀態,讓這個對象替你去做。這樣,你的代碼就會保持與其他代碼的實現的隔離,并增加你保持正交的機會。 l 避免使用全局數據。每當你的代碼引用全局數據時,它都把自己與共享該數據的其他組件綁在了一起。即使你只想對全局數據進行讀取,也可能會帶來麻煩(例如,如果你突然需要把代碼改為多線程的)。一般而言,如果你把所需的任何語境(context)顯式地傳入模塊,你的代碼就會更易于理解和維護。在面向對象應用中,語境常常作為參數傳給對象的構造器。換句話說,你可以創建含有語境的結構,并傳遞指向這些結構的引用。   《設計模式》[GHJV95]一書中的Singleton(單體)模式是確保特定類的對象只有一個實例的一種途徑。許多人把這些singleton對象用作某種全局變量(特別是在除此而外不支持全局概念的語言中,比如Java)。使用singleton要小心——它們可能造成不必要的關聯。 l 避免編寫相似的函數。你常常會遇到看起來全都很像的一組函數——它們也許在開始和結束處共享公共的代碼,中間的算法卻各有不同。重復的代碼是結構問題的一種癥狀。要了解更好的實現,參見《設計模式》一書中的Strategy(策略)模式。   養成不斷地批判對待自己的代碼的習慣。尋找任何重新進行組織、以改善其結構和正交性的機會。這個過程叫做重構(refactoring),它非常重要,所以我們專門寫了一節加以討論(見“重構”,184頁) 測試   正交地設計和實現的系統也更易于測試,因為系統的各組件間的交互是形式化的和有限的,更多的系統測試可以在單個的模塊級進行。這是好消息,因為與集成測試(integration testing)相比,模塊級(或單元)測試要更容易規定和進行得多。事實上,我們建議讓每個模塊都擁有自己的、內建在代碼中的單元測試,并讓這些測試作為常規構建過程的一部分自動運行(參見“易于測試的代碼”,189頁)。   構建單元測試本身是對正交性的一項有趣測試。要構建和鏈接某個單元測試,都需要什么?只是為了編譯或鏈接某個測試,你是否就必須把系統其余的很大一部分拽進來?如果是這樣,你已經發現了一個沒有很好地解除與系統其余部分耦合的模塊。   修正bug也是評估整個系統的正交性的好時候。當你遇到問題時,評估修正的局部化程度。 你是否只改動了一個模塊,或者改動分散在整個系統的各個地方?當你做出改動時,它修正了所有問題,還是又神秘地出現了其他問題?這是開始運用自動化的好機會。如果你使用了源碼控制系統(在閱讀了86頁的“源碼控制”之后,你會使用的),當你在測試之后、把代碼簽回(check the code back)時,標記所做的bug修正。隨后你可以運行月報,分析每個bug修正所影響的源文件數目的變化趨勢。 文檔   也許會讓人驚訝,正交性也適用于文檔。其坐標軸是內容和表現形式。對于真正正交的文檔,你應該能顯著地改變外觀,而不用改變內容。現代的字處理器提供了樣式表和宏,能夠對你有幫助(參見“全都是寫”,248頁)。 認同正交性   正交性與27頁介紹的DRY原則緊密相關。運用DRY原則,你是在尋求使系統中的重復降至最小;運用正交性原則,你可降低系統的各組件間的相互依賴。這樣說也許有點笨拙,但如果你緊密結合DRY原則、運用正交性原則,你將會發現你開發的系統會變得更為靈活、更易于理解、并且更易于調試、測試和維護。   如果你參加了一個項目,大家都在不顧一切地做出改動,而每一處改動似乎都會造成別的東西出錯,回想一下直升機的噩夢。項目很可能沒有進行正交的設計和編碼。是重構的時候了。   另外,如果你是直升機駕駛員,不要吃魚…… 相關內容: l 重復的危害,26頁 l 源碼控制,86頁 l 按合約設計,109頁 l 解耦與得墨忒耳法則,138頁 l 元程序設計,144頁 l 它只是視圖,157頁 l 重構,184頁 l 易于測試的代碼,189頁 l 邪惡的向導,198頁 l 注重實效的團隊,224頁 l 全都是寫,248頁 挑戰: l 考慮常在Windows系統上見到的面向GUI的大型工具和在shell提示下使用的短小、但卻可以組合的命令行實用工具。哪一種更為正交,為什么?如果正好按其設計用途加以應用,哪一種更易于使用?哪一種更易于與其他工具組合、以滿足新的要求? l C++支持多重繼承,而Java允許類實現多重接口。使用這些設施對正交性有何影響?使用多重繼承與使用多重接口的影響是否有不同?使用委托(delegation)與使用繼承之間是否有不同? 練習: 1. 你在編寫一個叫做Split的類,其用途是把輸入行拆分為字段。下面的兩個Java類的型構(signature)中,哪一個是更為正交的設計?  (解答在279頁) class Split1 { public Split1(InputStreamReader rdr) { ... public void readNextLine() throws IOException { ... public int numFields() { ... public String getField(int fieldNo) { ... } class Split2 { public Split2(String line) { ... public int numFields() { ... public String getField(int fieldNo) { ... } 2. 非模態對話框或模態對話框,哪一個能帶來更為正交的設計? (解答在279頁) 3. 過程語言與對象技術的情況又如何?哪一種能產生更為正交的系統? (解答在280頁) 9 可撤消性 如果某個想法是你惟一的想法,再沒有什么比這更危險的事情了。   ——Emil-Auguste Chartier, Propos sur la religion, 1938   工程師們喜歡問題有簡單、單一的解決方案。與論述法國大革命的無數起因的一篇模糊、熱烈的文章相比,允許你懷著極大的自信宣稱x = 2的數學測驗要讓人覺得舒服得多。管理人員往往與工程師趣味相投:單一、容易的答案正好可以放在電子表格和項目計劃中。   現實世界能夠合作就好了!遺憾的是,今天x是2,明天也許就需要是5,下周則是3。沒有什么永遠不變——而如果你嚴重依賴某一事實,你幾乎可以確定它將會變化。   要實現某種東西,總有不止一種方式,而且通常有不止一家供應商可以提供第三方產品。如果你參與的項目被短視的、認為只有一種實現方式的觀念所牽絆,你也許就會遇到讓人不悅的意外之事。許多項目團隊會被迫在未來展現之時睜開眼睛: “但你說過我們要使用XYZ數據庫!我們的項目已經完成了85%的編碼工作。我們現在不能改變了!”程序員抗議道。“對不起,但我們公司決定進行標準化,改用PDQ數據庫——所有項目。這超出了我的職權范圍。我們必須重新編碼。周末所有人都要加班,直到另行通知為止。”   變動不一定會這么嚴苛,甚至也不會這么迫在眉睫。但隨著時間的流逝,隨著你的項目取得進展,你也許會發現自己陷在無法立足的處境里。隨著每一項關鍵決策的做出,項目團隊受到越來越小的目標的約束——現實的更窄小的版本,選擇的余地越來越小。   在許多關鍵決策做出之后,目標會變得如此之小,以至于如果它動一下,或是風改變方向,或是東京的蝴蝶扇動翅膀,你都會錯過目標。而且你可能會偏出很遠。   問題在于,關鍵決策不容易撤消。   一旦你決定使用這家供應商的數據庫、那種架構模式、或是特定的部署模型(例如,客戶-服務器 vs. 單機),除非付出極大的代價,否則你就將受制于一個無法撤消的動作進程(course of action)。 可撤消性   我們讓本書的許多話題相互配合,以制作靈活、有適應能力的軟件。通過遵循它們的建議——特別是DRY原則(26頁)、解耦(138頁)以及元數據的使用(144頁)——我們不必做出許多關鍵的、不可逆轉的決策。這是一件好事情,因為我們并非總能在一開始就做出最好的決策。我們采用了某種技術,卻發現我們雇不到足夠的具有必需技能的人。我們剛剛選定某個第三方供應商,他們就被競爭者收購了。與我們開發軟件的速度相比,需求、用戶以及硬件變得更快。   假定在項目初期,你決定使用供應商A提供的關系數據庫。過了很久,在性能測試過程中,你發現數據庫簡直太慢了,而供應商B提供的對象數據庫更快。對于大多數傳統項目,你不會有什么運氣。大多數時候,對第三方產品的調用都纏繞在代碼各處。但如果你真的已經把數據庫的概念抽象出來——抽象到數據庫只是把持久(persistence)作為服務提供出來的程度——你就會擁有“中流換馬(change horses in midstream)”的靈活性。   與此類似,假定項目最初采用的是客戶-服務器模型,但隨即,在開發的后期,市場部門認為服務器對于某些客戶過于昂貴,他們想要單機版。對你來說,那會有多困難?因為這只是一個部署問題,所以不應該要很多天。如果所需時間更長,那么你就沒有考慮過可撤消性。另外一個方向甚至更有趣。如果需要以客戶-服務器或n層方式部署你正在開發的單機產品,事情又會怎樣?那也不應該很困難。   錯誤在于假定決策是澆鑄在石頭上的——同時還在于沒有為可能出現的意外事件做準備。 要把決策視為是寫在沙灘上的,而不要把它們刻在石頭上。大浪隨時可能到來,把它們抹去。 提示14: There Are No Final Decisions 不存在最終決策 靈活的架構   有許多人會設法保持代碼的靈活性,而你還需要考慮維持架構、部署及供應商集成等領域的靈活性。   像CORBA這樣的技術可以幫助把項目的某些部分與開發語言或平臺的變化隔離開來。Java在該平臺上的性能不能滿足要求?重新用C++編寫客戶代碼,其他沒有什么需要改變。用C++編寫的規則引擎不夠靈活?換到Smalltalk版本。采用CORBA架構,你只須改動替換的組件:其他組件應該不會受影響。   你正在開發UNIX軟件?哪一種?你是否處理了所有可移植性問題?你正在為某個特定版本的Windows做開發?哪一種——3.1、95、98、NT、CE、或是2000?支持其他版本有多難?如果你讓決策保持軟和與柔韌,事情就完全不困難。如果在代碼中有著糟糕的封裝、高度耦合以及硬編碼的邏輯或參數,事情也許就是不可能的。   不確定市場部門想怎樣部署系統?預先考慮這個問題,你可以支持單機、客戶-服務器、或n層模型——只需要改變配置文件。我們就寫過一些這么做的程序。   通常,你可以把第三方產品隱藏在定義良好的抽象接口后面。事實上,在我們做過的任何項目中,我們都總能夠這么做。但假定你無法那么徹底地隔離它,如果你必須大量地把某些語句分散在整個代碼中,該怎么辦?把該需求放入元數據,并且使用某種自動機制——比如Aspect(參見39頁)或Perl——把必需的語句插入代碼自身中。無論你使用的是何種機制,讓它可撤消。如果某樣東西是自動添加的,它也可以被自動去掉。   沒有人知道未來會怎樣,尤其是我們!所以要讓你的代碼學會“搖滾”:可以“搖”就“搖”,必須“滾”就“滾”。 相關內容: l 解耦與得墨忒耳法則,138頁 l 元程序設計,144頁 l 它只是視圖,157頁 挑戰: l 讓我們通過“薛定諤的貓”學一點量子力學。假定在一個封閉的盒子里有一只貓,還有一個放射性粒子。這個粒子正好有50%的機會裂變成兩個粒子。如果發生了裂變,貓就會被殺死;如果沒有,貓就不會有事。那么,貓是死是活?根據薛定諤的理論,正確的答案是“都是”。每當有兩種可能結果的亞核反應發生時,宇宙就會被克隆。在其中一個宇宙中,事件發生;在另一個宇宙中,事件不發生。貓在一個宇宙中是活的,在另一個宇宙中是死的。只有當你打開盒子,你才知道你在哪一個宇宙里。   怪不得為未來編碼很困難。   但想一想,代碼沿著與裝滿薛定諤的貓的盒子一樣的路線演化:每一項決策都會導致不同版本的未來。你的代碼能支持多少種可能的未來?哪一種未來更有可能發生?到時支持它們有多困難?   你敢打開盒子嗎? 10 曳光彈 預備、開火、瞄準……   在黑暗中用機槍射擊有兩種方式。你可以找出目標的確切位置(射程、仰角及方位)。你可以確定環境狀況(溫度、濕度、氣壓、風,等等)。你可以確定你使用的彈藥筒和子彈的精確規格,以及它們與你使用的機槍的交互作用。然后你可以用計算表或射擊計算機計算槍管的確切方向及仰角。如果每一樣東西都嚴格按照規定的方式工作,你的計算表正確無誤,而且環境沒有發生變化,你的子彈應該能落在距目標不遠的地方。   或者,你可以使用曳光彈。   曳光彈與常規彈藥交錯著裝在彈藥帶上。發射時,曳光彈中的磷點燃,在槍與它們擊中的地方之間留下一條煙火般的蹤跡。如果曳光彈擊中目標,那么常規子彈也會擊中目標。   并不讓人驚奇的是,曳光彈比費力計算更可取。反饋是即時的,而且因為它們工作在與真正的彈藥相同的環境中,外部影響得以降至最低。   這個類比也許有點暴力,但它適用于新的項目,特別是當你構建從未構建過的東西時。與槍手一樣,你也設法在黑暗中擊中目標。因為你的用戶從未見過這樣的系統,他們的需求可能會含糊不清。因為你在使用不熟悉的算法、技術、語言或庫,你面對著大量未知的事物。同時,因為完成項目需要時間,在很大程度上你能夠確知,你的工作環境將在你完成之前發生變化。   經典的做法是把系統定死。制作大量文檔,逐一列出每項需求、確定所有未知因素、并限定環境。根據死的計算射擊。預先進行一次大量計算,然后射擊并企望擊中目標。   然而,注重實效的程序員往往更喜歡使用曳光彈。 在黑暗中發光的代碼   曳光彈行之有效,是因為它們與真正的子彈在相同的環境、相同的約束下工作。它們快速飛向目標,所以槍手可以得到即時的反饋。同時,從實踐的角度看,這樣的解決方案也更便宜。   為了在代碼中獲得同樣的效果,我們要找到某種東西,讓我們能快速、直觀和可重復地從需求出發,滿足最終系統的某個方面要求。 提示15: Use Tracer Bullets to Find the Target 用曳光彈找到目標   有一次,我們接受了一個復雜的客戶-服務器數據庫營銷項目。其部分需求是要能夠指定并執行臨時查詢。服務器是一系列專用的關系數據庫。用Object Pascal編寫的客戶GUI使用一組C庫提供給服務器的接口。在轉換為優化的SQL之前,用戶的查詢以類似Lisp的表示方式存儲在服務器上;轉換直到執行前才進行。有許多未知因素和許多不同的環境,沒有人清楚地知道GUI應該怎樣工作。   這是使用曳光代碼的好機會。我們開發了前端框架、用于表示查詢的庫以及用于把所存儲的查詢轉換為具體數據庫的查詢的結構。隨后我們把它們集中在一起,并檢查它們是否能工作。使用最初構建的系統,我們所能做的只是提交一個查詢,列出某個表中的所有行,但它證明了UI能夠與庫交談,庫能夠對查詢進行序列化和解序列化,而服務器能夠根據結果生成SQL。在接下來的幾個月里,我們逐漸充實這個基本結構,通過并行地擴大曳光代碼的各個組件增加新的功能。當UI增加了新的查詢類型時,庫隨之成長,而我們也使SQL生成變得更為成熟。   曳光代碼并非用過就扔的代碼:你編寫它,是為了保留它。它含有任何一段產品代碼都擁有的完整的錯誤檢查、結構、文檔、以及自查。它只不過功能不全而已。但是,一旦你在系統的各組件間實現了端到端(end-to-end)的連接,你就可以檢查你離目標還有多遠,并在必要的情況下進行調整。一旦你完全瞄準,增加功能將是一件容易的事情。   曳光開發與項目永不會結束的理念是一致的:總有改動需要完成,總有功能需要增加。這是一個漸進的過程。   另一種傳統做法是一種繁重的工程方法:把代碼劃分為模塊,在真空中對模塊進行編碼。把模塊組合成子配件(subassembly),再對子配件進行組合,直到有一天你擁有完整的應用為止。直到那時,才能把應用作為一個整體呈現給用戶,并進行測試。   曳光代碼方法有許多優點: l 用戶能夠及早看到能工作的東西。如果你成功地就你在做的事情與用戶進行了交流(參見“極大的期望”,255頁),用戶就會知道他們看到的是還未完成的東西。他們不會因為缺少功能而失望;他們將因為看到了系統的某種可見的進展而欣喜陶醉。他們還會隨著項目的進展做出貢獻,增加他們的“買入”。同樣是這些用戶,他們很可能也會告訴你,每一輪“射擊”距離目標有多接近。 l 開發者構建了一個他們能在其中工作的結構。最令人畏縮的紙是什么也沒有寫的白紙。如果你已經找出應用的所有端到端的交互,并把它們體現在代碼里,你的團隊就無須再無中生有。這讓每個人都變得更有生產力,同時又促進了一致性。 l 你有了一個集成平臺。隨著系統端到端地連接起來,你擁有了一個環境,一旦新的代碼段通過了單元測試,你就可以將其加入該環境中。你將每天進行集成(常常是一天進行多次),而不是嘗試進行大爆炸式的集成。每一個新改動的影響都更為顯而易見,而交互也更為有限,于是調試和測試將變得更快、更準確。 l 你有了可用于演示的東西。項目出資人與高級官員往往會在最不方便的時候來看演示。有了曳光代碼,你總有東西可以拿給他們看。 l 你將更能夠感覺到工作進展。在曳光代碼開發中,開發者一個一個地處理用例(use case)。做完一個,再做下一個。評測性能、并向用戶演示你的進展,變得容易了許多。因為每一項個別的開發都更小,你也避免了創建這樣的整體式代碼塊:一周又一周,其完成度一直是95%。 曳光彈并非總能擊中目標   曳光彈告訴你擊中的是什么。那不一定總是目標。于是你調整準星,直到完全擊中目標為止。這正是要點所在。   曳光代碼也是如此。你在不能100%確定該去往何處的情形下使用這項技術。如果最初的幾次嘗試錯過了目標——用戶說:“那不是我的意思”,你需要的數據在你需要它時不可用,或是性能好像有問題——你不應感到驚奇。找出怎樣改變已有的東西、讓其更接近目標的辦法,并且為你使用了一種簡約的開發方法而感到高興。小段代碼的慣性也小——要改變它更容易、更迅速。你能夠搜集關于你的應用的反饋,而且與其他任何方法相比,你能夠花費較少代價、更為迅速地生成新的、更為準確的版本。同時,因為每個主要的應用組件都已表現在你的曳光代碼中,用戶可以確信,他們所看到的東西具有現實基礎,不僅僅是紙上的規范。 曳光代碼 vs. 原型制作   你也許會想,這種曳光代碼的概念就是原型制作,只不過有一個更富“進攻性”的名字。它們有區別。使用原型,你是要探究最終系統的某些具體的方面。使用真正的原型,在對概念進行了試驗之后,你會把你捆扎在一起的無論什么東西扔掉,并根據你學到的經驗教訓重新適當地進行編碼。   例如,假定你在制作一個應用,其用途是幫助運貨人確定怎樣把不規則的箱子裝入集裝箱。 除了考慮其他一些問題,你還需要設計直觀的用戶界面,而你用于確定最優裝箱方式的算法非常復雜。   你可以在GUI工具中為最終用戶制作一個用戶界面原型。你的代碼只能讓界面響應用戶操作。一旦用戶對界面布局表示同意,你可以把它扔掉,用目標語言重新對其進行編碼,并在其后加上商業邏輯。與此類似,你可以為實際進行裝箱的算法制作原型。你可以用像Perl這樣的寬松的高級語言編寫功能測試,并用更接近機器的某種語言編寫低級的性能測試。無論如何,一旦你做出決策,你都會重新開始在其最終環境中為算法編寫代碼,與現實世界接合。這就是原型制作,它非常有用。   曳光代碼方法處理的是不同的問題。你需要知道應用怎樣結合成一個整體。你想要向用戶演示,實際的交互是怎樣工作的,同時你還想要給出一個架構骨架,開發者可以在其上增加代碼。在這樣的情況下,你可以構造一段曳光代碼,其中含有一個極其簡單的集裝箱裝箱算法實現(也許是像“先來先服務”這樣的算法)和一個簡單、但卻能工作的用戶界面。一旦你把應用中的所有組件都組合在一起,你就擁有了一個可以向你的用戶和開發者演示的框架。接下來的時間里,你給這個框架增加新功能,完成預留了接口的例程。但框架仍保持完整,而你也知道,系統將會繼續按照你第一次的曳光代碼完成時的方式工作。   其間的區別很重要,足以讓我們再重復一次。原型制作生成用過就扔的代碼。曳光代碼雖然簡約,但卻是完整的,并且構成了最終系統的骨架的一部分。你可以把原型制作視為在第一發曳光彈發射之前進行的偵察和情報搜集工作。 相關內容: l 足夠好的軟件,9頁 l 原型與便箋,53頁 l 規范陷阱,217頁 l 極大的期望,255頁 11 原型與便箋   許多不同的行業都使用原型試驗具體的想法:與完全的制作相比,制作原型要便宜得多。例如,轎車制造商可以制造某種新車設計的許多不同的原型,每一種的設計目的都是要測試轎車的某個具體的方面——空氣動力學、樣式、結構特征,等等。也許會制造一個粘土模型,用于風洞測試,也許會為工藝部門制造一個輕木和膠帶模型,等等。有些轎車公司更進一步,在計算機上進行大量的建模工作,從而進一步降低了開銷。以這樣的方式,可以試驗危險或不確定的元件,而不用實際進行真實的制造。   我們以同樣的方式構建軟件原型,并且原因也一樣——為了分析和揭示風險,并以大大降低的代價、為修正提供機會。與轎車制造商一樣,我們可以把原型用于測試項目的一個或多個具體方面。   我們往往以為原型要以代碼為基礎,但它們并不總是非如此不可。與轎車制造商一樣,我們可以用不同的材料構建原型。要為像工作流和應用邏輯這樣的動態事物制作原型,便箋(post-it note)就非常好。用戶界面的原型則可以是白板上的圖形、或是用繪圖程序或界面構建器繪制的無功能的模型。   原型的設計目的就是回答一些問題,所以與投入使用的產品應用相比,它們的開發要便宜得多、快捷得多。其代碼可以忽略不重要的細節——在此刻對你不重要,但對后來的用戶可能非常重要。例如,如果你在制作GUI原型,你不會因不正確的結果或數據而遭到指責。而另一方面,如果你只是在研究計算或性能方面的問題,你也不會因為相當糟糕的GUI而遭到指責;甚至也可以完全不要GUI。   但如果你發現自己處在不能放棄細節的環境中,就需要問自己,是否真的在構建原型。或許曳光彈開發方式更適合這種情況(參見“曳光彈”,48頁)。 應制作原型的事物   你可以選擇通過原型來研究什么樣的事物呢?任何帶有風險的事物。以前沒有試過的事物,或是對于最終系統極端關鍵的事物。任何未被證明的、實驗性的、或有疑問的事物。任何讓你覺得不舒服的事物。你可以為下列事物制作原型: l 架構 l 已有系統中的新功能 l 外部數據的結構或內容 l 第三方工具或組件 l 性能問題 l 用戶界面設計 >   原型制作是一種學習經驗。其價值并不在于所產生的代碼,而在于所學到的經驗教訓。那才是原型制作的要點所在。 提示16: Prototype to Learn 為了學習而制作原型 怎樣使用原型   在構建原型時,你可以忽略哪些細節? l 正確性。你也許可以在適當的地方使用虛設的數據。 l 完整性。原型也許只能在非常有限的意義上工作,也許只有一項預先選擇的輸入數據和一個菜單項。 l 健壯性。錯誤檢查很可能不完整,或是完全沒有。如果你偏離預定路徑,原型就可能崩潰,并在“煙火般的燦爛顯示中焚毀”。這沒有關系。 l 風格。在紙上承認這一點讓人痛苦,但原型代碼可能沒有多少注釋或文檔。根據使用原型的經驗,你也許會撰寫出大量文檔,但關于原型系統自身的內容相對而言卻非常少。   因為原型應該遮蓋細節,并聚焦于所考慮系統的某些具體方面,你可以用非常高級的語言實現原型——比項目的其余部分更高級(也許是像Perl、Python或Tcl這樣的語言)。高級的腳本語言能讓你推遲考慮許多細節(包括指定數據類型),并且仍然能制作出能工作的(即使不完整或速度慢)代碼。如果你需要制作用戶界面的原型,可研究像Tcl/Tk、Visual Basic、Powerbuilder或Delphi這樣的工具。   作為能把低級的部分組合在一起的“膠合劑”,腳本語言工作良好。在Windows下,Visual Basic可以把COM控件膠合在一起。更一般地說,你可以使用像Perl和Python這樣的語言,把低級的C庫綁在一起——無論是手工進行,還是通過工具自動進行,比如可以自由獲取的SWIG[URL 28]。采用這種方法,你可以快速地把現有組件裝配進新的配置,從而了解它們的工作情況。 制作架構原型   許多原型被構造出來,是要為在考慮之下的整個系統建模。與曳光彈不同,在原型系統中,單個模塊不需要能行使特定的功能。事實上,要制作架構原型,你甚至不一定需要進行編碼——你可以用便箋或索引卡片、在白板上制作原型。你尋求的是了解系統怎樣結合成為一個整體,并推遲考慮細節。下面是一些你可以在架構原型中尋求解答的具體問題: 1. 主要組件的責任是否得到了良好定義? 2. 是否適當? 3. 主要組件間的協作是否得到了良好定義? 4. 耦合是否得以最小化? 5. 你能否確定重復的潛在來源? 6. 接口定義和各項約束是否可接受? 7. 每個模塊在執行過程中是否能訪問到其所需的數據?是否能在需要時進行訪問? 根據我們制作原型的經驗,最后一項往往會產生最讓人驚訝和最有價值的結果。 怎樣“不”使用原型   在你著手制作任何基于代碼的原型之前,先確定每個人都理解你正在編寫用過就扔的代碼。對于不知道那只是原型的人,原型可能會具有欺騙性的吸引力。你必須非常清楚地說明,這些代碼是用過就扔的,它們不完整,也不可能完整。   別人很容易被演示原型外表的完整性誤導,而如果你沒有設定正確的期望值,項目出資人或管理部門可能會堅持要部署原型(或其后裔)。提醒他們,你可以用輕木和膠帶制造一輛了不起的新車原型,但你卻不會在高峰時間的車流中駕駛它。   如果你覺得在你所在的環境或文化中,原型代碼的目的很有可能被誤解,你也許最好還是采用曳光彈方法。你最后將得到一個堅實的框架,為將來的開發奠定基礎。   適當地使用原型,可以幫助你在開發周期的早期確定和改正潛在的問題點——在此時改正錯誤既便宜、又容易——從而為你節省大量時間、金錢,并大大減輕你遭受的痛苦和折磨。 相關內容: l 我的源碼讓貓給吃了,2頁 l 交流!,18頁 l 曳光彈,48頁 l 極大的期望,255頁 練習: 4. 市場部門想要坐下來和你一起討論一些網頁的設計問題。他們想用可點擊的圖像進行頁面導航,但卻不能確定該用什么圖像模型——也許是轎車、電話或是房子。你有一些目標網頁和內容;他們想要看到一些原型。哦,隨便說一下,你只有15分鐘。你可以使用什么樣的工具?  (解答在280頁) 12 領域語言 語言的界限就是一個人的世界的界限。   ——維特根斯坦   計算機語言會影響你思考問題的方式,以及你看待交流的方式。每種語言都含有一系列特性——比如靜態類型與動態類型、早期綁定與遲后綁定、繼承模型(單、多或無)這樣的時髦話語——所有這些特性都在提示或遮蔽特定的解決方案。頭腦里想著Lisp設計的解決方案將會產生與基于C風格的思考方式而設計的解決方案不同的結果,反之亦然。與此相反——我們認為這更重要——問題領域的語言也可能會提示出編程方案。   我們總是設法使用應用領域的語匯來編寫代碼(參見210頁的需求之坑,我們在那里提出要使用項目詞匯表)。在某些情況下,我們可以更進一層,采用領域的語匯、語法、語義——語言——實際進行編程。   當你聽取某個提議中的系統的用戶說明情況時,他們也許能確切地告訴你,系統應怎樣工作: 在一組X.25線路上偵聽由ABC規程12.3定義的交易,把它們轉譯成XYZ公司的43B格式,在衛星上行鏈路上重新傳輸,并存儲起來,供將來分析使用。   如果用戶有一些這樣的做了良好限定的陳述,你可以發明一種為應用領域進行了適當剪裁的小型語言,確切地表達他們的需要: From X25LINE1 (Format=ABC123) { Put TELSTAR1 (Format=XYZ43B); Store DB; }   該語言無須是可執行的。一開始,它可以只是用于捕捉用戶需求的一種方式——一種規范。但是,你可能想要更進一步,實際實現該語言。你的規范變成了可執行代碼。   在你編寫完應用之后,用戶給了你一項新需求:不應存儲余額為負的交易,而應以原來的格式在X.25線路上發送回去: From X25LINE1 (Format=ABC123) { if (ABC123.balance < 0) { Put X25LINE1 (Format=ABC123); } else { Put TELSTAR1 (Format=XYZ43B); Store DB; } }   很容易,不是嗎?有了適當的支持,你可以用大大接近應用領域的方式進行編程。我們并不是在建議讓你的最終用戶用這些語言實際編程。相反,你給了自己一個工具,能夠讓你更靠近他們的領域工作。 提示17: Program Close to the Problem domain 靠近問題領域編程   無論是用于配置和控制應用程序的簡單語言,還是用于指定規則或過程的更為復雜的語言,我們認為,你都應該考慮讓你的項目更靠近問題領域。通過在更高的抽象層面上編碼,你獲得了專心解決領域問題的自由,并且可以忽略瑣碎的實現細節。   記住,應用有許多用戶。有最終用戶,他們了解商業規則和所需輸出;也有次級用戶:操作人員、配置與測試管理人員、支持與維護程序員,還有將來的開發者。他們都有各自的問題領域,而你可以為他們所有人生成小型環境和語言。 具體領域的錯誤   如果你是在問題領域中編寫程序,你也可以通過用戶可以理解的術語進行具體領域的驗證,或是報告問題。以上一頁我們的交換應用為例,假定用戶拼錯了格式名: From X25LINE1 (Format=AB123)   如果這發生在某種標準的、通用的編程語言中,你可能會收到一條標準的、通用的錯誤消息: Syntax error: undeclared identifier   但使用小型語言,你卻能夠使用該領域的語匯發出錯誤消息: "AB123" is not a format. known formats are ABC123, XYZ43B, PDQB, and 42. 實現小型語言   在最簡單的情況下,小型語言可以采用面向行的、易于解析的格式。在實踐中,與其他任何格式相比,我們很可能會更多地使用這樣的格式。只要使用switch語句、或是使用像Perl這樣的腳本語言中的正則表達式,就能夠對其進行解析。281頁上練習5的解答給出了一種用C編寫的簡單實現。   你還可以用更為正式的語法,實現更為復雜的語言。這里的訣竅是首先使用像BNF這樣的表示法定義語法。一旦規定了文法,要將其轉換為解析器生成器(parser generator)的輸入語法通常就非常簡單了。C和C++程序員多年來一直在使用yacc(或其可自由獲取的實現,bison[URL 27])。在Lex and Yacc[LMB92]一書中詳細地講述了這些程序。Java程序員可以選用javaCC,可在[URL 26]處獲取該程序。282頁上練習7的解答給出了一個用bison編寫的解析器。如其所示,一旦你了解了語法,編寫簡單的小型語言實在沒有多少工作要做。   要實現小型語言還有另一種途徑:擴展已有的語言。例如,你可以把應用級功能與Python[URL 9]集成在一起,編寫像這樣的代碼: record = X25LINE1.get(format=ABC123) if (lance < 0): X25LINE1.put(record, format=ABC123) else: TELSTAR1.put(record, format=XYZ43B) DB.store(record) 數據語言與命令語言   可以通過兩種不同的方式使用你實現的語言。   數據語言產生某種形式的數據結構給應用使用。這些語言常用于表示配置信息。   例如,sendmail程序在世界各地被用于在Internet上轉發電子郵件。它具有許多杰出的特性和優點,由一個上千行的配置文件控制,用sendmail自己的配置語言編寫: Mlocal, P=/usr/bin/procmail, F=lsDFMAw5 :/|@qSPfhn9, S=10/30, R=20/40, T=DNS/RFC822/X-Unix, A=procmail -Y -a $h -d $u   顯然,可讀性不是sendmail的強項。   多年以來,Microsoft一直在使用一種可以描述菜單、widget(窗口小部件)、對話框及其他Windows資源的數據語言。下一頁上的圖2.2摘錄了一段典型的資源文件。這比sendmail的配置文件要易讀得多,但其使用方式卻完全一樣——我們編譯它,以生成數據結構。   命令語言更進了一步。在這種情況下,語言被實際執行,所以可以包含語句、控制結構、以及類似的東西(比如58頁上的腳本)。 圖2.2 Windows .rc文件   你也可以使用自己的命令語言來使程序易于維護。例如,也許用戶要求你把來自某個遺留應用的信息集成進你的新GUI開發中。要完成這一任務,常用的方法是“刮屏”(screen scraping):你的應用連接到主機應用,就好像它是正常的使用人員;發出鍵擊,并“閱讀”取回的響應。你可以使用一種小型語言來把這樣的交互編寫成腳本: locate prompt "SSN:" type "%s" social_security_number type enter waitfor keyboardunlock if text_at(10,14) is "INVALID SSN" return bad_ssn if text_at(10,14) is "DUPLICATE SSN" return dup_ssn # etc... 當應用確定是時候輸入社會保障號時,它調用解釋器執行這個腳本,后者隨即對事務進行控制。如果解釋器是嵌入在應用中的,兩者甚至可以直接共享數據(例如,通過回調機制)。   這里你是在維護程序員(maintenace programmer)的領域中編程。當主機應用發生變化、字段移往別處時,程序員只需更新你的高級描述,而不用鉆入C代碼的各種細節中。 獨立語言與嵌入式語言   要發揮作用,小型語言無須由應用直接使用。許多時候,我們可以使用規范語言創建各種由程序自身編譯、讀入或用于其他用途的制品(包括元數據。參見元程序設計,144頁)。   例如,在100頁我們將描述一個系統,在其中我們使用Perl、根據原始的schema規范生成大量衍生物。我們發明了一種用于表示數據庫schema的通用語言,然后生成我們所需的所有形式——SQL、C、網頁、XML,等等。應用不直接使用規范,但它依賴于根據規范產生的輸出。   把高級命令語言直接嵌入你的應用是一種常見做法,這樣,它們就會在你的代碼運行時執行。這顯然是一種強大的能力;通過改變應用讀取的腳本,你可以改變應用的行為,卻完全不用編譯。這可以顯著地簡化動態的應用領域中的維護工作。 易于開發還是易于維護   我們已經看到若干不同的文法,范圍從簡單的面向行的格式到更為復雜的、看起來像真正的語言的文法。既然實現更為復雜的文法需要額外的努力,你又為何要這樣做呢?   權衡要素是可擴展性與維護。盡管解析“真正的”語言所需的代碼可能更難編寫,但它卻容易被人理解得多,并且將來用新特性和新功能進行擴展也要容易得多。太簡單的語言也許容易解析,但卻可能晦澀難懂——很像是60頁上的sendmail例子。   考慮到大多數應用都會超過預期的使用期限,你可能最好咬緊牙關,先就采用更復雜、可讀性更好的語言。最初的努力將在降低支持與維護費用方面得到許多倍的回報。 相關內容: l 元程序設計,144頁 挑戰: l 你目前的項目的某些需求是否能以具體領域的語言表示?是否有可能編寫編譯器或轉譯器,生成大多數所需代碼? l 如果你決定采用小型語言作為更接近問題領域的編程方式,你就是接受了,實現它們需要一些努力。你能否找到一些途徑,通過它們把你為某個項目開發的框架復用于其他項目? 練習: 5. 我們想實現一種小型語言,用于控制一種簡單的繪圖包(或許是一種“海龜圖形”(turtle-graphics)系統)。這種語言由單字母命令組成。有些命令后跟單個數字。例如,下面的輸入將會繪制出一個矩形: P 2 # select pen 2 D # pen down W 2 # draw west 2cm N 1 # then north 1 E 2 # then east 2 S 1 # then back south U # pen up   請實現解析這種語言的代碼。它應該被設計成能簡單地增加新命令。(解答在281頁) 6. 設計一種解析時間規范的BNF文法。應能接受下面的所有例子:(解答在282頁) 4pm, 7:38pm, 23:42, 3:16, 3:16am 7. 用yacc、bison或類似的解析器生成器為練習6中的BNF文法實現解析器。(解答在282頁) 8. 用Perl實現時間解析器(提示:正則表達式可帶來好的解析器)。(解答在283頁) 13 估算   快!通過56k modem線發送《戰爭與和平》需要多少時間?存儲一百萬個姓名與地址需要多少磁盤空間?1 000字節的數據塊通過路由器需要多少時間?交付你的項目需要多少個月?   在某種程度上,這些都是沒有意義的問題——它們都缺少信息。然而它們仍然可以得到回答,只要你習慣于進行估算。同時,在進行估算的過程中,你將會加深對你的程序所處的世界的理解。   通過學習估算,并將此技能發展到你對事物的數量級有直覺的程度,你就能展現出一種魔法般的能力,確定它們的可行性。當有人說“我們將通過ISDN線路把備份發給中央站點”時,你將能夠直覺地知道那是否實際。當你編碼時,你將能夠知道哪些子系統需要優化,哪些可以放在一邊。 提示18: Estimate to Avoid Surprises 估算,以避免發生意外   作為獎勵,在這一節的末尾我們將透露一個總是正確的答案——無論什么時候有人要你進行估算,你都可以給出答案。 多準確才足夠準確   在某種程度上,所有的解答都是估算。只不過有一些要比其他的更準確。所以當有人要你進行估算時,你要問自己的第一個問題就是,你解答問題的語境是什么?他們是需要高度的準確性,還是在考慮棒球場的大小? l 如果你的奶奶問你何時抵達,她也許只是想知道該給你準備午餐還是晚餐。而一個困在水下、空氣就快用光的潛水員很可能對精確到秒的答案更感興趣。 l p的值是多少?如果你想知道的是要買多少飾邊,才能把一個圓形花壇圍起來,那么“3”很可能就足夠好了。如果你在學校里,那么“22/7”也許就是一個好的近似值。如果你在NASA(美國國家航空航天管理局),那么也許要12個小數位。   關于估算,一件有趣的事情是,你使用的單位會對結果的解讀造成影響。如果你說,某事需要130個工作日,那么大家會期望它在相當接近的時間里完成。但是,如果你說“哦,大概要六個月”,那么大家知道它會在從現在開始的五到七個月內完成。這兩個數字表示相同的時長,但“130天”卻可能暗含了比你的感覺更高的精確程度。我們建議你這樣度量時間估算: 時長 報出估算的單位 1-15天 天 3-8周 周 8-30周 月 30+周 在給出估算前努力思考一下   于是,在完成了所有必要的工作之后,你確定項目將需要125個工作日(25周),你可以給出“大約六個月”的估算。   同樣的概念適用于對任何數量的估算:要選擇能反映你想要傳達的精確度的單位。 13 估算(2) 估算來自哪里   所有的估算都以問題的模型為基礎。但在我們過深地卷入建模技術之前,我們必須先提及一個基本的估算訣竅,它總能給出好的答案:去問已經做過這件事情的人。在你一頭鉆進建模之前,仔細在周圍找找也曾處在類似情況下的人。   看看他們的問題是怎么解決的。你不大可能找到完全相符的案例,但你會驚奇有多少次,你能夠成功地借鑒他人的經驗。 理解提問內容   任何估算練習的第一步都是建立對提問內容的理解。除了上面討論的精確度問題以外,你還需要把握問題域的范圍。這常常隱含在問題中,但你需要養成在開始猜想之前先思考范圍的習慣。常常,你選擇的范圍將形成你給出的解答的一部分:“假定沒有交通意外,而且車里還有汽油,我會在20分鐘內趕到那里。” 建立系統的模型   這是估算有趣的部分。根據你對所提問題的理解,建立粗略、就緒的思維模型骨架。如果你是在估算響應時間,你的模型也許要涉及服務器和某種到達流量(arriving traffic)。對于一個項目,模型可以是你的組織在開發過程中所用的步驟、以及系統的實現方式的非常粗略的圖景。   建模既可以是創造性的,又可以是長期有用的。在建模的過程中,你常常會發現一些在表面上不明顯的底層模式與過程。你甚至可能會想要重新檢查原來的問題:“你要求對做X所需的時間進行估算。但好像X的變種Y只需一半時間就能完成,而你只會損失一個特性。”   建模把不精確性引入了估算過程中。這是不可避免的,而且也是有益的。你是在用模型的簡單性與精確性做交易。使花在模型上的努力加倍也許只能帶來精確性的輕微提高。你的經驗將告訴你何時停止提煉。 把模型分解為組件   一旦擁有了模型,你可以把它分解為組件。你須要找出描述這些組件怎樣交互的數學規則。有時某個組件會提供一個值,加入到結果中。有些組件有著成倍的影響,而另一些可能會更為復雜(比如那些模擬某個節點上的到達流量的組件)。   你將會發現,在典型情況下,每個組件都有一些參數,會對它給整個模型帶來什么造成影響。在這一階段,只要確定每個參數就行了。 給每個參數指定值   一旦你分解出各個參數,你就可以逐一給每個參數賦值。在這個步驟中你可能會引入一些錯誤。訣竅是找出哪些參數對結果的影響最大,并致力于讓它們大致正確。在典型情況下,其值被直接加入結果的參數,沒有被乘或除的那些參數重要。讓線路速度加倍可以讓1小時內接收的數據量加倍,而增加5毫秒的傳輸延遲不會有顯著的效果。   你應該采用一種合理的方式計算這些關鍵參數。對于排隊的例子,你可以測量現有系統的實際事務到達率,或是找一個類似的系統進行測量。與此類似,你可以測量現在服務1個請求所花的時間,或是使用這一節描述的技術進行估算。事實上,你常常會發現自己以其他子估算為基礎進行估算。這是最大的錯誤伺機溜進來的地方。 計算答案   只有在最簡單的情況下估算才有單一的答案。你也許會高興地說:“我能在15分鐘內走完五個街區。”但是,當系統變得更為復雜時,你就會避免做出正面回答。進行多次計算,改變關鍵參數的值,直到你找出真正主導模型的那些參數。電子表格可以有很大幫助。然后根據這些參數表述你的答案。“如果系統擁有SCSI總線和64MB內存,響應時間約為四分之三秒;如果內存是48MB,則響應時間約為一秒。”(注意“四分之三秒”怎樣給人以一種與750毫秒不同的精確感。)   在計算階段,你可能會得到看起來很奇怪的答案。不要太快放棄它們。如果你的運算是正確的,那你對問題或模型的理解就很可能是錯的。這是非常寶貴的信息。 追蹤你的估算能力   我們認為,記錄你的估算,從而讓你看到自己接近正確答案的程度,這是一個非常好的主意。如果總體估算涉及子估算的計算,那么也要追蹤這些子估算。你常常會發現自己估算得非常好——事實上,一段時間之后,你就會開始期待這樣的事情。   如果結果證明估算錯了,不要只是聳聳肩走開。找出事情為何與你的猜想不同的原因。也許你選擇了與問題的實際情況不符的一些參數。也許你的模型是錯的。不管原因是什么,花一點時間揭開所發生的事情。如果你這樣做了,你的下一次估算就會更好。 估算項目進度   在面對相當大的應用開發的各種復雜問題與反復無常的情況時,普通的估算規則可能會失效。我們發現,為項目確定進度表的惟一途徑常常是在相同的項目上獲取經驗。如果你實行增量開發、重復下面的步驟,這不一定就是一個悖論: l 檢查需求 l 分析風險 l 設計、實現、集成 l 向用戶確認   一開始,你對需要多少次迭代、或是需要多少時間,也許只有模糊的概念。有些方法要求你把這個作為初始計劃的一部分定下來,但除了最微不足道的項目,這是一個錯誤。除非你在開發與前一個應用類似的應用,擁有同樣的團隊和同樣的技術,否則,你就只不過是在猜想。   于是你完成了初始功能的編碼與測試,并將此標記為第一輪增量開發的結束。基于這樣的經驗,你可以提煉你原來對迭代次數、以及在每次迭代中可以包含的內容的猜想。提煉會變得一次比一次好,對進度表的信心也將隨之增長。 提示19: Iterate the Schedule with the Code 通過代碼對進度表進行迭代   這也許并不會受到管理部門的歡迎,在典型情況下,他們想要的是單一的、必須遵守的數字——甚至是在項目開始之前。你必須幫助他們了解團隊、團隊的生產率、還有環境將決定進度。通過使其形式化,并把改進進度表作為每次迭代的一部分,你將給予他們你所能給予的最精確的進度估算。 在被要求進行估算時說什么   你說:“我等會兒回答你。”   如果你放慢估算的速度,并花一點時間仔細檢查我們在這一節描述的步驟,你幾乎總能得到更好的結果。在咖啡機旁給出的估算將(像咖啡一樣)回來糾纏你。 相關內容: l 算法速度,177頁 挑戰: l 開始寫估算日志。追蹤每一次估算的精確程度。如果你的錯誤率大于50%,設法找出你的估算誤入歧途的地方。 練習: 9. 有人問你:“1Mbps的通信線路和在口袋里裝了4GB磁帶、在兩臺計算機間步行的人,哪一個的帶寬更高?”你要對你的答案附加什么約束,以確保你的答復的范圍是正確的?(例如,你可以說,訪問磁帶所花時間忽略不計。) (解答在283頁) 10. 那么,哪一個帶寬更高? (解答在284頁) 14 純文本的威力   每個工匠在開始其職業生涯時,都會準備一套品質良好的基本工具。木匠可能需要尺、計量器、幾把鋸子、幾把好刨子、精良的鑿子、鉆孔器和夾子、錘子還有鉗子。這些工具將經過認真挑選、打造得堅固耐用、并用于完成很少與其他工具重合的特定工作,而且,也許最重要的是,剛剛出道的木匠把它們拿在手里會覺得很順手。   隨后學習與適應的過程就開始了。每樣工具都有自身的特性和古怪之處,并且需要得到相應的特殊對待。每樣工具都需要以獨特的方式進行打磨,或者以獨特的方式把持。隨著時間的過去,每樣工具都會因使用而磨損,直到手柄看上去就像是木匠雙手的模子,而切割面與握持工具的角度完全吻合。到這時,工具變成了工匠的頭腦與所完成的產品之間的通道——它們變成了工匠雙手的延伸。木匠將不時增添新的工具,比如餅式切坯機、激光制導斜切鋸、楔形模具——全都是奇妙的技術,但你可以肯定的是,當他把原來的某樣工具拿在手里,當他聽到刨子滑過木料發出的歌聲時,那是他最高興的時候。   工具放大你的才干。你的工具越好,你越是能更好地掌握它們的用法,你的生產力就越高。從一套基本的通用工具開始,隨著經驗的獲得,隨著你遇到一些特殊需求,你將會在其中增添新的工具。要與工匠一樣,想著定期增添工具。要總是尋找更好的做事方式。如果你遇到某種情況,你覺得現有的工具不能解決問題,記得去尋找可能會有幫助的其他工具或更強大的工具。 讓需要驅動你的采購。   許多新程序員都會犯下錯誤,采用單一的強力工具,比如特定的集成開發環境(IDE),而且再也不離開其舒適的界面。這實在是個錯誤。我們要樂于超越IDE所施加的各種限制。要做到這一點,惟一的途徑是保持基本工具集的“鋒利”與就緒。   在本章我們將討論怎樣為你自己的基本工具箱投資。與關于工具的任何好的討論一樣,我們將從考察你的原材料——你將要制作的東西——開始(在“純文本的威力”中)。然后我們將從那里轉向工作臺(workbench),在我們的工作范圍也就是計算機。要怎樣使用計算機,你才能最大限度地利用你所用的工具?我們將在shell游戲中討論這一問題。現在我們有了工作所需的材料及工作臺,我們將轉向一樣你可能用得最頻繁的工具:你的編輯器。在強力編輯中,我們將提出多種讓你更有效率的途徑。   為了確保不會丟失先前的任何工作成果,我們應該總是使用源碼控制系統——即使是像我們的個人地址簿這樣的東西!同時,因為Murphy先生實在是一個樂觀主義者,如果你沒有高超的調試技能,你就不可能成為了不起的程序員。   你需要一些“膠合劑”,把大量魔術“粘”在一起。我們將在文本操縱中討論一些可能的方案,比如awk、Perl以及Python。   就如同木匠有時會制作模具,用以控制復雜工件的打造一樣,程序員也可以編寫自身能編寫代碼的代碼。我們將在“代碼生成器”中討論這一問題。   花時間學習使用這些工具,有一天你將會驚奇地發現,你的手指在鍵盤上移動,操縱文本,卻不用進行有意識的思考。工具將變成你的雙手的延伸。 純文本的威力   作為注重實效的程序員,我們的基本材料不是木頭,不是鐵,而是知識。我們搜集需求,將其變為知識,隨后又在我們的設計、實現、測試、以及文檔中表達這些知識。而且我們相信,持久地存儲知識的最佳格式是純文本。通過純文本,我們給予了自己既能以手工方式、也能以程序方式操縱知識的能力——實際上可以隨意使用每一樣工具。 什么是純文本   純文本由可打印字符組成,人可以直接閱讀和理解其形式。例如,盡管下面的片段由可打印字符組成,它卻是無意義的: Fieldl9=467abe   閱讀者不知道467abe的含義是什么。更好的選擇是讓其變得能讓人理解: DrawingType=UMLActivityDrawing   純文本并非意味著文本是無結構的;XML、SGML和HTML都是有良好定義的結構的純文本的好例子。通過純文本,你可以做你通過某種二進制格式所能做的每件事情,其中包括版本管理。   與直接的二進制編碼相比,純文本所處的層面往往更高;前者通常直接源自實現。假定你想要存儲叫做uses_menus的屬性,其值既可為TRUE,也可為FALSE。使用純文本,你可以將其寫為: es_menus=FALSE   把它與0010010101110101對比一下。   大多數二進制格式的問題在于,理解數據所必需的語境與數據本身是分離的。你人為地使數據與其含義脫離開來。數據也可能加了密;沒有應用邏輯對其進行解析,這些數據絕對沒有意義。但是,通過純文本,你可以獲得自描述(self-describing)的、不依賴于創建它的應用的數據流。 提示20: Keep Knowledge in Plain Text 用純文本保存知識 缺點   使用純文本有兩個主要缺點:(1)與壓縮的二進制格式相比,存儲純文本所需空間更多,(2)要解釋及處理純文本文件,計算上的代價可能更昂貴。   取決于你的應用,這兩種情況或其中之一可能讓人無法接受——例如,在存儲衛星遙測數據時,或是用做關系數據庫的內部格式時。   但即使是在這些情況下,用純文本存儲關于原始數據的元數據也可能是可以接受的(參見“元程序設計”,144頁)。   有些開發者可能會擔心,用純文本存儲元數據,是在把這些數據暴露給系統的用戶。這種擔心放錯了地方。與純文本相比,二進制數據也許更晦澀難懂,但卻并非更安全。如果你擔心用戶看到密碼,就進行加密。如果你不想讓他們改變配置參數,就在文件中包含所有參數值的安全哈希值作作為校驗和。 文本的威力   既然更大和更慢不是用戶最想要的特性,為什么還要使用純文本?好處是什么? l 保證不過時 l 杠桿作用 l 更易于測試 保證不過時   人能夠閱讀的數據形式,以及自描述的數據,將比所有其他的數據形式和創建它們的應用都活得更長久。句號。   只要數據還存在,你就有機會使用它——也許是在原來創建它的應用已經不存在很久之后。   只需部分地了解其格式,你就可以解析這樣的文件;而對于大多數二進制文件,要成功地進行解析,你必須了解整個格式的所有細節。   考慮一個來自某遺留系統的數據文件。關于原來的應用你的了解很少;對你來說最要緊的是它保存了客戶的社會保障號列表,你需要找出這些保障號,并將其提取出來。在數據文件中,你看到: <FIELD10>123-45-6789</FIELD10> ... <FIELD10>567-89-0123</FIELD10> ... <FIELD10>901-23-4567</FIELD10>   識別出了社會保障號的格式,你可以很快寫一個小程序提取該數據——即使你沒有關于文件中其他任何東西的信息。   但設想一下,如果該文件的格式是這樣的: AC27123456789B11P ... XY43567890123QTYL ... 6T2190123456788AM   你可能就不會那么輕松地識別出這些數字的含義了。這是人能夠閱讀(human readable)與人能夠理解(human understandable)之間的區別。   在我們進行解析時,FIELD10的幫助也不大。改成 <SSNO>123-45-6789</SSNO> 就會讓這個練習變得一點也不費腦子——而且這些數據保證會比創建它的任何項目都活得更長久。 杠桿作用   實際上,計算世界中的每一樣工具,從源碼管理系統到編譯器環境,再到編輯器及獨立的過濾器,都能夠在純文本上進行操作。 Unix哲學   提供“鋒利”的小工具、其中每一樣都意在把一件事情做好——Unix因圍繞這樣的哲學進行設計而著稱。這一哲學通過使用公共的底層格式得以實行:面向行的純文本文件。用于系統管理(用戶及密碼、網絡配置,等等)的數據庫全都作為純文本文件保存(有些系統,比如Solaris,為了優化性能,還維護有特定數據的二進制形式。純文本版本保留用作通往二進制版本的接口)。   當系統崩潰時,你可能需要通過最小限度的環境進行恢復(例如,你可能無法訪問圖形驅動程序)。像這樣的情形,實在可以讓你欣賞到純文本的簡單性。   例如,假定你要對一個大型應用進行產品部署,該應用具有復雜的針對具體現場的配置文件(我們想到sendmail)。如果該文件是純文本格式的,你可以把它置于源碼控制系統的管理之下(參見源碼控制,86頁),這樣你就可以自動保存所有改動的歷史。像diff和fc這樣的文件比較工具允許你查看做了哪些改動,而sum允許你生成校驗和,用以監視文件是否受到了偶然的(或惡意的)修改。 更易于測試   如果你用純文本創建用于驅動系統測試的合成數據,那么增加、更新、或是修改測試數據就是一件簡單的事情,而且無須為此創建任何特殊工具。與此類似,你可以非常輕松地分析回歸測試(regression test)輸出的純文本,或通過Perl、Python及其他腳本工具進行更為全面徹底的檢查。 最小公分母   即使在未來,基于XML的智能代理已能自治地穿越混亂、危險的Internet、自行協商數據交換,無處不在的純文本也仍然會存在。事實上,在異種環境中,純文本的優點比其所有的缺點都重要。你需要確保所有各方能夠使用公共標準進行通信。純文本就是那個標準。 相關內容: l 源碼控制,86頁 l 代碼生成器,102頁 l 元程序設計,144頁 l 黑板,165頁 l 無處不在的自動化,230頁 l 全都是寫,248頁 挑戰: l 使用你喜歡的語言,用直接的二進制表示設計一個小地址簿數據庫(姓名、電話號碼,等等)。完成以后再繼續往下讀。 1. 把該格式轉換成使用XML的純文本格式。 2. 在這兩個版本中,增加一個新的、叫做方向的變長字段,在其中你可以輸入每個人的住宅所在的方向。   在版本管理與可擴展性方面會遇到什么問題?哪種形式更易于修改?轉換已有的數據呢? 15 shell游戲   每個木匠都需要好用、堅固、可靠的工作臺,用以在加工工件時把工件放置在方便的高度上。工作臺成為木工房的中心,隨著工件的成形,木匠會一次次回到工作臺的近旁。   對于操縱文本文件的程序員,工作臺就是命令shell。在shell提示下,你可以調用你的全套工具,并使用管道、以這些工具原來的開發者從未想過的方式把它們組合在一起。在shell下,你可以啟動應用、調試器、瀏覽器、編輯器以及各種實用程序。你可以搜索文件、查詢系統狀態、過濾輸出。通過對shell進行編程,你可以構建復雜的宏命令,用來完成你經常進行的各種活動。   對于在GUI界面和集成開發環境(IDE)上成長起來的程序員,這似乎顯得很極端。畢竟,用鼠標指指點點,你不是也同樣能把這些事情做好嗎?   簡單的回答:“不能”。GUI界面很奇妙,對于某些簡單操作,它們也可能更快、更方便。移動文件、閱讀MIME編碼的電子郵件以及寫信,這都是你可能想要在圖形環境中完成的事情。但如果你使用GUI完成所有的工作,你就會錯過你的環境的某些能力。你將無法使常見任務自動化,或是利用各種可用工具的全部力量。同時,你也將無法組合你的各種工具,創建定制的宏工具。GUI的好處是WYSIWYG——所見即所得(what you see is what you get)。缺點是WYSIAYG——所見即全部所得(what you see is all you get)。   GUI環境通常受限于它們的設計者想要提供的能力。如果你需要超越設計者提供的模型,你大概不會那么走運——而且很多時候,你確實需要超越這些模型。注重實效的程序員并非只是剪切代碼、或是開發對象模型、或是撰寫文檔、或是使構建過程自動化——所有這些事情我們全都要做。通常,任何一樣工具的適用范圍都局限于該工具預期要完成的任務。例如,假定你需要把代碼預處理器集成進你的IDE中(為了實現按合約設計、多處理編譯指示,等等)。除非IDE的設計者明確地為這種能力提供了掛鉤,否則,你無法做到這一點。   你也許已經習慣于在命令提示下工作,在這種情況下,你可以放心地跳過這一節。否則,你也許還需要我們向你證明,shell是你的朋友。   作為注重實效的程序員,你不斷地想要執行特別的操作——GUI可能不支持的操作。當你想要快速地組合一些命令,以完成一次查詢或某種其他的任務時,命令行要更為適宜。這里有一些例子: 找出修改日期比你的Makefile的修改日期更近的全部.c文件。 Shell ` find . -name ' *.c' –newer Makefile –print` GUI 打開資源管理器,轉到正確的目錄,點擊Makefile,記下修改時間。然后調出 “工具/查找”,在指定文件處輸入*.c。選擇“日期”選項卡,在第一個日期字段中輸入你記下的Makefile的日期。然后點擊“確定”。 構造我的源碼的zip/tar存檔文件。 Shell zip p *.h *.c 或 tar cvf r *.h *.c GUI 調出ZIP實用程序(比如共享軟件WinZip[URL 41]),選擇[創建新存檔文件],輸入它的名稱,在“增加”對話框中選擇源目錄,把過濾器設置為“*.c”,點擊“增加”,把過濾器設置為“*.h”,點擊“增加”,然后關閉存檔文件。 在上周哪些Java文件沒有改動過? Shell find . -name '*.java' -mtime +7 –print GUI 點擊并轉到“查找文件”,點擊“文件名”字段,敲入“*.java”,選擇“修改日期”選項卡。然后選擇“介于”。點擊“開始日期”,敲入項目開始的日期。點擊“結束日期”,敲入1周以前的日期(確保手邊有日歷)。點擊“開始查找”。 上面的文件中,哪些使用了awt庫? Shell find . -name '*.java' -mtime +7 -print | xargs grep 't' GUI 把前面的例子列出的各個文件裝入編輯器,搜索字符串“Jt”。把含有該字符串的文件的名字寫下來。   顯然,這樣的例子還可以一直舉下去。shell命令可能很晦澀,或是太簡略,但卻很強大,也很簡練。同時,因為shell命令可被組合進腳本文件(或是Windows下的命令文件)中,你可以構建命令序列,使你常做的事情自動化。 提示21: Use the Power of Command Shells 利用命令shell的力量   去熟悉shell,你會發現自己的生產率迅速提高。需要創建你的Java代碼顯式導入的全部軟件包的列表(重復的只列出一次)?下面的命令將其存儲在叫做“list”的文件中: grep '^import ' *.java | sed -e's/.*import *//' -e's/;.*$//' | sort -u >list   如果你沒有花大量時間研究過你所用系統上的命令shell的各種能力,這樣的命令會顯得很嚇人。但是,投入一些精力去熟悉你的shell,事情很快就會變得清楚起來。多使用你的命令shell,你會驚訝它能使你的生產率得到怎樣的提高。 shell實用程序與Windows系統   盡管隨Windows系統提供的命令shell在逐步改進,Windows命令行實用程序仍然不如對應的Unix實用程序。但是,并非一切都已無可挽回。   Cygnus Solutions公司有一個叫做Cygwin[URL 31]的軟件包。除了為Windows提供Unix兼容層以外,Cygwin還帶有120多個Unix實用程序,包括像ls、grep和find這樣的很受歡迎的程序。你可以自由下載并使用這些實用程序和庫,但一定要閱讀它們的許可。隨同Cygwin發布的還有Bash shell。 在Windows下使用Unix工具   在Windows下有高質量的Unix工具可用,這讓我們很高興;我們每天都使用它們。但是,要注意存在一些集成問題。與對應的MS-DOS工具不同,這些實用程序對文件名的大小寫敏感,所以ls a*.bat不會找到AUTOEXEC.BAT。你還可能遇到含有空格的文件名、或是路徑分隔符不同所帶來的問題。最后,在Unix shell下運行需要MS-DOS風格的參數的MS-DOS程序時,會發生一些有趣的問題。例如,在Unix下,來自JavaSoft的Java實用程序使用冒號作為CLASSPATH分隔符,而在MS-DOS下使用的卻是分號。結果,運行在Unix機器上的Bash或ksh腳本在Windows下也同樣能運行,但它傳給Java的命令行卻會被錯誤地解釋。   另外,David Korn(因Korn shell而聞名)制作了一個叫做UWIN的軟件包。其目標與Cygwin相同——它是Windows下的Unix開發環境。UWIN帶有Korn shell的一個版本。也可從Global Technologies, Ltd.[URL 30]獲取商業版本。此外,AT&T提供了該軟件包的自由下載版本,用于評估和學術研究。再次說明,在使用之前要先閱讀它們的許可。   最后,Tom Christiansen(在本書撰寫的同時)正在制作Perl Power Tools,嘗試用Perl可移植地實現所有常見的Unix實用程序[URL 32]。 相關內容: l 無處不在的自動化,230頁 挑戰: l 你目前是否在GUI中用手工做一些事情?你是否曾將一些說明發給同事,其中涉及許多“點這個按鈕”、“選哪一項”之類的步驟?它們能自動化嗎? l 每當你遷往新環境時,要找出可以使用的shell。看是否能把現在使用的shell帶過去。 l 調查各種可用于替換你現在的shell的選擇。如果你遇到你的shell無法處理的問題,看其他shell是否能更好地應對。 16 強力編輯   先前我們說過,工具是手的延伸。噢,與任何其他軟件工具相比,這都更適用于編輯器。你需要能盡可能不費力氣地操縱文本,因為文本是編程的基本原材料。讓我們來看一些能幫助你最大限度地利用編輯環境的一些常見特性和功能。 一種編輯器   我們認為你最好是精通一種編輯器,并將其用于所有編輯任務:代碼、文檔、備忘錄、系統管理,等等。如果不堅持使用一種編輯器,你就可能會面臨現代的巴別塔大混亂。你可能必須用每種語言的IDE內建的編輯器進行編碼,用“all-in-one”辦公軟件編輯文檔,或是用另一種內建的編輯器發送電子郵件。甚至你用于在shell中編輯命令行的鍵擊都有可能不同。如果你在每種環境中有不同的編輯約定和命令,要精通這些環境中的任何一種都會很困難。   你需要的是精通。只是依次輸入、并使用鼠標進行剪貼是不夠的。那樣,在你的手中有了一個強大的編輯器,你卻無法發揮出它的效能。敲擊十次<-或BACKSPACE,把光標左移到行首,不會像敲擊一次^A、Home或0那樣高效。 提示22: Use a Single Editor Well 用好一種編輯器   選一種編輯器,徹底了解它,并將其用于所有的編輯任務。如果你用一種編輯器(或一組鍵綁定)進行所有的文本編輯活動,你就不必停下來思考怎樣完成文本操縱:必需的鍵擊將成為本能反應。編輯器將成為你雙手的延伸;鍵會在滑過文本和思想時歌唱起來。這就是我們的目標。   確保你選擇的編輯器能在你使用的所有平臺上使用。Emacs、vi、CRiSP、Brief及其他一些編輯器可在多種平臺上使用,并且常常既有GUI版本,也有非GUI(文本屏幕)版本。 編輯器特性   除了你認為特別有用、使用時特別舒適的特性之外,還有一些基本能力,我們認為每個像樣的編輯器都應該具備。如果你的編輯器缺少其中的任何能力,那么你或許就應該考慮換一種更高級的編輯器了。 l 可配置。編輯器的所有方面都應該能按你的偏好(preference)配置,包括字體、顏色、窗口尺寸以及鍵擊綁定(什么鍵執行什么命令)。對于常見的編輯操作,與鼠標或菜單驅動的命令相比,只使用鍵擊效率更高,因為你的手無須離開鍵盤。 l 可擴展。編輯器不應該只因為出現了新的編程語言就變得過時。它應該能集成你在使用的任何編譯器環境。你應該能把任何新語言或文本格式(XML、HTML第9版,等等)的各種細微差別“教”給它。 l 可編程。你應該能對編輯器編程,讓它執行復雜的、多步驟的任務。可以通過宏或內建的腳本編程語言(例如,Emacs使用了Lisp的一個變種)進行這樣的編程。 此外,許多編輯器支持針對特定編程語言的特性,比如: l 語法突顯 l 自動完成 l 自動縮進 l 初始代碼或文檔樣板 l 與幫助系統掛接 l 類IDE特性(編譯、調試,等等)   像語法突顯這樣的特性聽起來也許像是無關緊要的附加物,但實際上卻可能非常有用,而且還能提高你的生產率。一旦你習慣了看到關鍵字以不同的顏色或字體出現,遠在你啟動編譯器之前,沒有以那樣的方式出現的、敲錯的關鍵字就會在你面前跳出來。   對于大型項目,能夠在編輯器環境中進行編譯、并直接轉到出錯處非常方便。Emacs特別擅長進行這種方式的交互。 生產率   我們遇到的用Windows notepad編輯源碼的人數量驚人。這就像是把茶匙當做鐵鍬——只是敲鍵和使用基本的基于鼠標的剪貼是不夠的。   有什么樣的事情需要你做,你卻無法以這樣的方式做到呢?   嗯,讓我們以光標移動的例子作為開始。與重復擊鍵、一個字符一個字符或一行一行移動相比,按一次鍵、就以詞、行、塊或函數為單位移動光標,效率要高得多。   再假設你在編寫Java代碼。你想要按字母順序排列import語句,而另外有人簽入(check in)了一些文件,沒有遵守這一標準(這聽起來也許很極端,但在大型項目中,這可以讓你節省大量時間,不用逐行檢查一大堆import語句)。你想要快速地從頭到尾檢查一些文件,并對它們的一小部分區域進行排序。在像vi和Emacs這樣的編輯器中,你可以很容易完成這樣的任務(參見圖3.1)。用notepad試試看! 圖3.1 在編輯器中對文本行進行排序   有些編輯器能幫助你使常用操作流水線化。例如,當你創建特定語言的新文件時,編輯器可以為你提供模板。其中也許包括: l 填好的類名或模塊名(根據文件名派生) l 你的姓名和/或版權聲明 l 該語言中的各種構造體(construct)的骨架(例如,構造器與析構器聲明)   自動縮進是另一種有用的特性。你不必(使用空格或tab)進行手工縮進,編輯器會自動在適當的時候(例如,在敲入左花括號時)為你進行縮進。這一特性讓人愉快的地方是,你可以用編輯器為你的項目提供一致的縮進風格[20]。 然后做什么   這種建議特別難寫,因為實際上每個讀者對他們所用編輯器的熟悉程度和相關經驗都有所不同。那么,作為總結,并為下一步該做什么提出一些指導方針,在下面的左邊一欄中找到與你的情況相符的情況,然后看右邊一欄,看你應該做什么。 如果這聽起來像你…… 那么考慮…… 我使用許多不同的編輯器,但只使用其基本特性。 選一種強大的編輯器,好好學習它。 我有最喜歡的編輯器,但不使用其全部特性。 學習它們。減少你需要敲擊的鍵數。 我有最喜歡的編輯器,只要可能就使用它。 設法擴展它,并將其用于比現在更多的任務。 我認為你們在胡說。notepad就是有史以來最好的編輯器。 只要你愿意,并且生產率很高,那就這樣吧!但如果你發現自己在“羨慕”別人的編輯器,你可能就需要重新評估自己的位置了。 有哪些編輯器可用   此前我們建議你掌握一種像樣的編輯器,那么我們推薦哪種編輯器呢?嗯,我們要回避這個問題;你對編輯器的選擇是一個個人問題(有人甚至會說這是個“信仰問題”!)。但是,在附錄A(266頁)中,我們列出了許多流行的編輯器和獲取它們的途徑。 挑戰: l 有些編輯器使用完備的語言進行定制和腳本編寫。例如,Emacs采用了Lisp。作為本年度你將學習的新語言之一,學習你的編輯器使用的語言。如果你發現自己在重復做任何事情,開發一套宏(或等價的東西)加以處理。 l 你是否知道你的編輯器所能做的每一件事情?設法難倒使用同樣的編輯器的同事。設法通過盡可能少的鍵擊完成任何給定的編輯任務。 17 源碼控制 進步遠非由變化組成,而是取決于好記性。不能記住過去的人,被判重復過去。   ——George Santayana, Life of Reason   我們在用戶界面中找尋的一個重要的東西是UNDO鍵——一個能原諒我們的錯誤的按鈕。如果環境支持多級撤消(undo)與重做(redo),那就更好了,這樣你就可以回去,撤消幾分鐘前發生的事情。但如果錯誤發生在上周,而你那以后已經把計算機打開關閉了十次呢?噢,這是使用源碼控制系統的諸多好處之一:它是一個巨大的UNDO鍵——一個項目級的時間機器,能夠讓你返回上周的那些太平日子,那時的代碼還能夠編譯并運行。   源碼控制系統(或范圍更寬泛的配置管理系統)追蹤你在源碼和文檔中做出的每一項變動。 更好的系統還能追蹤編譯器及OS版本。有了適當配置的源碼控制系統,你就總能夠返回你的軟件的前一版本。   但源碼控制系統(SCCS)能做的遠比撤消錯誤要多。好的SCCS讓你追蹤變動,回答這樣的問題:誰改動了這一行代碼?在當前版本與上周的版本之間有什么區別?在這次發布的版本中我們改動了多少行代碼?哪個文件改動最頻繁?對于bug追蹤、審計、性能及質量等目的,這種信息非常寶貴。   SCCS還能讓你標識你的軟件的各次發布。一經標識,你將總是能夠返回并重新生成該版本,并且不受在其后發生的變動的影響。   我們常常使用SCCS管理開發樹中的分支。例如,一旦你發布了某個軟件,你通常會想為下一次發布繼續開發。與此同時,你也需要處理當前發布的版本中的bug,把修正后的版本發送給客戶。(如果合適)你想要讓這些bug修正合并進下一次發布中,但你不想把正在開發的代碼發送給客戶。通過SCCS,在每次生成一個發布版本時,你可以在開發樹中生成分支。你把bug修正加到分支中的代碼上,并在主干上繼續開發。因為bug修正也可能與主干有關,有些系統允許你把選定的來自分支的變動自動合并回主干中。   源碼控制系統可能會把它們維護的文件保存在某個中央倉庫(repository)中——這是進行存檔的好候選地。   最后,有些產品可能允許兩個或更多用戶同時在相同的文件集上工作,甚至在同一文件中同時做出改動。系統隨后在文件被送回倉庫時對這些改動進行合并。盡管看起來有風險,在實踐中這樣的系統在所有規模的項目上都工作良好。 提示23: Always Use Source Code Control 總是使用源碼控制   總是。即使你的團隊只有你一個人,你的項目只需一周時間;即使那是“用過就扔”的原型;即使你的工作對象并非源碼;確保每樣東西都處在源碼控制之下——文檔、電話號碼表、給供應商的備忘錄、makefile、構建與發布流程、燒制CD母盤的shell小腳本——每樣東西。我們例行公事地對我們敲入的每一樣東西進行源碼控制(包括本書的文本)。即使我們不是在開發項目,我們的日常工作也被安全地保存在倉庫中。 源碼控制與構建   把整個項目置于源碼控制系統的保護之下具有一項很大的、隱蔽的好處:你可以進行自動的和可重復的產品構建。   項目構建機制可以自動從倉庫中取出最近的源碼。它可以在午夜運行,在每個人都(很可能)回家之后。你可以運行自動的回歸測試,確保當日的編碼沒有造成任何破壞。構建的自動化保證了一致性——沒有手工過程,而你也不需要開發者記住把代碼拷貝進特殊的構建區域。   構建是可重復的,因為你總是可以按照源碼將給定日期的內容重新進行構建。 但我們團隊沒有使用源碼控制   他們應該感到羞恥!聽起來這是個“布道”的機會!但是,在等待他們看到光明的同時,也許你應該實施自己私人的源碼控制。使用我們在附錄A中列出的可自由獲取的工具,并確保把你個人的工作安全地保存進倉庫中(并且完成你的項目所要求的無論什么事情)。盡管這看起來像是重復勞動,我們幾乎可以向你擔保,在你須要回答像“你對xyz模塊做了什么?”和“是什么破壞了構建?”這樣的問題時,它將使你免受困擾(并為你的項目節省金錢)。這一方法也許還能有助于使你們的管理部門確信,源碼控制確實行之有效。   不要忘了,SCCS也同樣適用于你在工作之外所做的事情。 源碼控制產品   附錄A(271頁)給出了一些有代表性的源碼控制系統的URL,有些是商業產品,有些可自由獲取。還有許多其他的產品可用——你可以在配置管理FAQ中尋求建議。 相關內容: l 正交性,34頁 l 純文本的力量,73頁 l 全都是寫,248頁 挑戰: l 即使你無法在工作中使用SCCS,也要在個人的系統上安裝RCS或CVS。用它管理你的“寵物項目”、你撰寫的文檔、以及(可能的)應用于計算機系統自身的配置變動。 l 在Web上有些開放源碼項目的存檔對外公開(比如Mozilla[URL51]、KDE[URL54]、以及Gimp[URL55]),看一看這樣的項目。你怎樣獲取源文件的更新?你怎樣做出改動?——項目是否會對訪問進行管制,或是對改動的并入進行裁決? 18 調試 這是痛苦的事: 看著你自己的煩憂,并且知道 不是別人、而是你自己一人所致   ——索福克勒斯:《埃阿斯》   自從14世紀以來,bug(蟲子、臭蟲)一詞就一直被用于描述“恐怖的東西”。COBOL的發明者,海軍少將Grace Hopper博士據信觀察到了第一只計算機bug——真的是一只蟲子,一只在早期計算機系統的繼電器里抓到的蛾子。在被要求解釋機器為何未按期望運轉時,有一位技術人員報告說,“有一只蟲子在系統里”,并且負責地把它——翅膀及其他所有部分——粘在了日志簿里。   遺憾的是,在我們的系統里仍然有“bug”,雖然不是會飛的那種。但與以前相比,14世紀的含義——可怕的東西——現在也許更為適用。軟件缺陷以各種各樣的方式表現自己,從被誤解的需求到編碼錯誤。糟糕的是,現代計算機系統仍然局限于做你告訴它的事情,而不一定是你想要它做的事情。   沒有人能寫出完美的軟件,所以調試肯定要占用你大量時間。讓我們來看一看調試所涉及的一些問題,以及一些用于找出難以捉摸的蟲子的一般策略。 調試的心理學   對于許多開發者,調試本身是一個敏感、感性的話題。你可能會遇到抵賴、推諉、蹩腳的借口、甚或是無動于衷,而不是把它當做要解決的難題發起進攻。   要接受事實:調試就是解決問題,要據此發起進攻。   發現了他人的bug之后,你可以花費時間和精力去指責讓人厭惡的肇事者。在有些工作環境中,這是文化的一部分,并且可能是“疏通劑”。但是,在技術競技場上,你應該專注于修正問題,而不是發出指責。 提示24: Fix the Problem, Not the Blame 要修正問題,而不是發出指責   bug是你的過錯還是別人的過錯,并不是真的很有關系。它仍然是你的問題。 調試的思維方式 最容易欺騙的人是一個人自己。   ——Edward Bulwer-Lytton, The Disowned   在你開始調試之前,選擇恰當的思維方式十分重要。你須要關閉每天用于保護自我(ego)的許多防衛措施,忘掉你可能面臨的任何項目壓力,并讓自己放松下來。最重要的是,記住調試的第一準則: 提示25: Don’t Panic 不要恐慌   人很容易恐慌,特別是如果你正面臨最后期限的到來、或是正在設法找出bug的原因,有一個神經質的老板或客戶在你的脖子后面喘氣。但非常重要的事情是,要后退一步,實際思考什么可能造成你認為表征了bug的那些癥狀。   如果你目睹bug或見到bug報告時的第一反應是“那不可能”,你就完全錯了。一個腦細胞都不要浪費在以“但那不可能發生”起頭的思路上,因為很明顯,那不僅可能,而且已經發生了。   在調試時小心“近視”。要抵制只修正你看到的癥狀的急迫愿望:更有可能的情況是,實際的故障離你正在觀察的地方可能還有幾步遠,并且可能涉及許多其他的相關事物。要總是設法找出問題的根源,而不只是問題的特定表現。 從何處開始   在開始查看bug之前,要確保你是在能夠成功編譯的代碼上工作——沒有警告。我們例行公事地把編譯器警告級設得盡可能高。把時間浪費在設法找出編譯器能夠為你找出的問題上沒有意義!我們需要專注于手上更困難的問題。   在設法解決任何問題時,你需要搜集所有的相關數據。糟糕的是,bug報告不是精密科學。你很容易被巧合誤導,而你不能承受把時間浪費在對巧合進行調試上。你首先需要在觀察中做到準確。   bug報告的準確性在經過第三方之手時會進一步降低——實際上你可能需要觀察報告bug的用戶的操作,以獲取足夠程度的細節。   Andy曾經參與過一個大型圖形應用的開發。快要發布時,測試人員報告說,每次他們用特定的畫筆畫線,應用都會崩潰。負責該應用的程序員爭辯說,這個畫筆沒有任何問題;他試過用它繪圖,它工作得很好。幾天里這樣的對話來回進行,大家的情緒急速上升。   最后,我們讓他們坐到同一個房間里。測試人員選了畫筆工具,從右上角到左下角畫了一條線。應用程序炸了。“噢”,程序員用很小的聲音說。他隨后像綿羊一樣承認,他在測試時只測試了從左下角畫到右上角的情況,沒有暴露出這個bug。   這個故事有兩個要點: l 你也許需要與報告bug的用戶面談,以搜集比最初給你的數據更多的數據。 l 人工合成的測試(比如那個程序員只從下畫到上)不能足夠地演練(exercise)應用。你必須既強硬地測試邊界條件,又測試現實中的最終用戶的使用模式。你需要系統地進行這樣的測試(參見無情的測試,237頁)。 測試策略   一旦你認為你知道了在發生什么,就到了找出程序認為在發生什么的時候了。 再現bug(reproduction,亦有“繁殖”之意——譯注)   不,我們的bug不會真的繁殖(盡管其中有一些可能已經到了合法的生育年齡)。我們談論的是另一種“再現”。   開始修正bug的最佳途徑是讓其可再現。畢竟,如果你不能再現它,你又怎么知道它已經被修正了呢?   但我們想要的不是能夠通過長長的步驟再現的bug;我們要的是能夠通過一條命令再現的bug。如果你必須通過15個步驟才能到達bug顯露的地方,修正bug就會困難得多。有時候,強迫你自己隔離顯示出bug的環境,你甚至會洞見到它的修正方法。   要了解沿著這些思路延伸的其他想法,參見無處不在的自動化(230頁)。 使你的數據可視化   常常,要認識程序在做什么——或是要做什么——最容易的途徑是好好看一看它操作的數據。最簡單的例子是直截了當的“variable name = data value”方法,這可以作為打印文本、也可以作為GUI對話框或列表中的字段實現。   但通過使用允許你“使數據及其所有的相互關系可視化”的調試器,你可以深入得多地獲得對你的數據的洞察。有一些調試器能夠通過虛擬現實場景把你的數據表示為3D立交圖,或是表示為3D波形圖,或是就表示為簡單的結構圖(如下一頁的圖3.2所示)。在單步跟蹤程序的過程中,當你一直在追獵的bug突然跳到你面前時,這樣的圖遠勝于千言萬語。   即使你的調試器對可視化數據的支持有限,你仍然自己進行可視化——或是通過手工方式,用紙和筆,或是用外部的繪圖程序。   DDD調試器有一些可視化能力,并且可以自由獲取(參見[URL 19])。有趣的是,DDD能與多種語言一起工作,包括Ada、C、C++、Fortran、Java、Modula、Pascal、 圖3.2 一個循環鏈表的調試器示例圖。箭頭表示指向節點的指針 Perl以及Python(顯然是正交的設計)。 跟蹤   調試器通常會聚焦于程序現在的狀態。有時你需要更多的東西——你需要觀察程序或數據結構隨時間變化的狀態。查看棧蹤跡(stack trace)只能告訴你,你是怎樣直接到達這里的。它無法告訴你,在此調用鏈之前你在做什么,特別是在基于事件的系統中。   跟蹤語句把小診斷消息打印到屏幕上或文件中,說明像“到了這里”和“x的值 = 2”這樣的事情。與IDE風格的調試器相比,這是一種原始的技術,但在診斷調試器無法診斷的一些錯誤種類時卻特別有效。在時間本身是一項因素的任何系統中,跟蹤都具有難以估量的價值:并發進程、實時系統、還有基于事件的應用。   你可以使用跟蹤語句“鉆入”代碼。也就是,你可以在沿著調用樹下降時增加跟蹤語句。   跟蹤消息應該采用規范、一致的格式:你可能會想自動解析它們。例如,如果你需要跟蹤資源泄漏(比如未配平(unbalanced)的open/close),你可以把每一次open和每一次close 記錄在日志文件中。通過用Perl處理該日志文件,你可以輕松地確定 壞變量?檢查它們的鄰居   有時你檢查一個變量,希望看到一個小整數值,得到的卻是像0x6e69614d這樣的東西。在你卷起袖子、鄭重其事地開始調試之前,先快速地查看一下這個壞變量周圍的內存。這常常能帶給你線索。在我們的例子中,把周邊的內存作為字符進行檢查得到的是: 20333231 6e69614d 2c745320 746f4e0a 1 2 3 M a i n S t , \n N o t 2c6e776f 2058580a 31323433 00000a33 o w n , \n x x 3 4 2 1 3\n\0\0   看上去像是有人把街道地址“噴”到了我們的計數器上。現在我們知道該去查看什么地方了。 有問題的open是在哪里發生的。 橡皮鴨   找到問題的原因的一種非常簡單、卻又特別有用的技術是向別人解釋它。他應該越過你的肩膀看著屏幕,不斷點頭(像澡盆里上下晃動的橡皮鴨)。他們一個字也不需要說;你只是一步步解釋代碼要做什么,常常就能讓問題從屏幕上跳出來,宣布自己的存在。   這聽起來很簡單,但在向他人解釋問題時,你必須明確地陳述那些你在自己檢查代碼時想當然的事情。因為必須詳細描述這些假定中的一部分,你可能會突然獲得對問題的新洞見。 消除過程   在大多數項目中,你調試的代碼可能是你和你們團隊的其他成員編寫的應用代碼、第三方產品(數據庫、連接性、圖形庫、專用通信或算法,等等)、以及平臺環境(操作系統、系統庫、編譯器)的混合物。   bug有可能存在于OS、編譯器、或是第三方產品中——但這不應該是你的第一想法。有大得多的可能性的是,bug存在于正在開發的應用代碼中。與假定庫本身出了問題相比,假定應用代碼對庫的調用不正確通常更有好處。即使問題確實應歸于第三方,在提交bug報告之前,你也必須先消除你的代碼中的bug。   我們參加過一個項目的開發,有位高級工程師確信select系統調用在Solaris上有問題。再多的勸說或邏輯也無法改變他的想法(這臺機器上的所有其他網絡應用都工作良好這一事實也一樣無濟于事)。他花了數周時間編寫繞開這一問題的代碼,因為某種奇怪的原因,卻好像并沒有解決問題。當最后被迫坐下來、閱讀關于select的文檔時,他在幾分鐘之內就發現并糾正了問題。現在每當有人開始因為很可能是我們自己的故障而抱怨系統時,我們就會使用“select沒有問題”作為溫和的提醒。 提示26: “Select” Isn’t Broken “Select”沒有問題   記住,如果你看到馬蹄印,要想到馬,而不是斑馬。OS很可能沒有問題。數據庫也很可能情況良好。   如果你“只改動了一樣東西”,系統就停止了工作,那樣東西很可能就需要對此負責——直接地或間接地,不管那看起來有多牽強。有時被改動的東西在你的控制之外:OS的新版本、編譯器、數據庫或是其他第三方軟件都可能會毀壞先前的正確代碼。可能會出現新的bug。你先前已繞開的bug得到了修正,卻破壞了用于繞開它的代碼。API變了,功能變了;簡而言之,這是全新的球賽,你必須在這些新的條件下重新測試系統。所以在考慮升級時要緊盯著進度表;你可能會想等到下一次發布之后再升級。   但是,如果沒有顯而易見的地方讓你著手查看,你總是可以依靠好用的老式二分查找。看癥狀是否出現在代碼中的兩個遠端之一,然后看中間。如果問題出現了,則臭蟲位于起點與中點之間;否則,它就在中點與終點之間。以這種方式,你可以讓范圍越來越小,直到最終確定問題所在。 造成驚訝的要素   在發現某個bug讓你吃驚時(也許你在用我們聽不到的聲音咕噥說:“那不可能。”),你必須重新評估你確信不疑的“事實”。在那個鏈表例程中——你知道它堅固耐用,不可能是這個bug的原因——你是否測試了所有邊界條件?另外一段代碼你已經用了好幾年——它不可能還有bug。可能嗎?   當然可能。某樣東西出錯時,你感到吃驚的程度與你對正在運行的代碼的信任及信心成正比。這就是為什么,在面對“讓人吃驚”的故障時,你必須意識到你的一個或更多的假設是錯的。不要因為你“知道”它能工作而輕易放過與bug有牽連的例程或代碼。證明它。用這些數據、這些邊界條件、在這個語境中證明它。 提示27: Don’t Assume it – Prove It 不要假定,要證明   當你遇到讓人吃驚的bug時,除了只是修正它而外,你還需要確定先前為什么沒有找出這個故障。考慮你是否需要改進單元測試或其他測試,以讓它們有能力找出這個故障。   還有,如果bug是一些壞數據的結果,這些數據在造成爆發之前傳播通過了若干層面,看一看在這些例程中進行更好的參數檢查是否能更早地隔離它(分別參見120頁與122頁的關于早崩潰及斷言的討論)。   在你對其進行處理的同時,代碼中是否有任何其他地方容易受這同一個bug的影響?現在就是找出并修正它們的時機。確保無論發生什么,你都知道它是否會再次發生。   如果修正這個bug需要很長時間,問問你自己為什么。你是否可以做點什么,讓下一次修正這個bug變得更容易?也許你可以內建更好的測試掛鉤,或是編寫日志文件分析器。   最后,如果bug是某人的錯誤假定的結果,與整個團隊一起討論這個問題。如果一個人有誤解,那么許多人可能也有。   去做所有這些事情,下一次你就將很有希望不再吃驚。 調試檢查列表 l 正在報告的問題是底層bug的直接結果,還是只是癥狀? l bug真的在編譯器里?在OS里?或者是在你的代碼里? l 如果你向同事詳細解釋這個問題,你會說什么? l 如果可疑代碼通過了單元測試,測試是否足夠完整?如果你用該數據運行單元測試,會發生什么? l 造成這個bug的條件是否存在于系統中的其他任何地方? 相關內容: l 斷言式編程,122頁 l 靠巧合編程,172頁 l 無處不在的自動化,230頁 l 無情的測試,237頁 挑戰: l 調試已經夠有挑戰性了。 19 文本操縱   注重實效的程序員用與木匠加工木料相同的方式操縱文本。在前面的部分里,我們討論了我們所用的一些具體工具——shell、編輯器、調試器。這些工具與木匠的鑿子、鋸子、刨子類似——它們都是用于把一件或兩件工作做好的專用工具。但是,我們不時也需要完成一些轉換,這些轉換不能由基本工具集直接完成。我們需要通用的文本操縱工具。   文本操縱語言對于編程的意義,就像是刳刨機(router)對于木工活的意義。它們嘈雜、骯臟、而且有點用“蠻力”。如果使用有誤,整個工件都可能毀壞。有人發誓說在工具箱里沒有它們的位置。但在恰當的人的手中,刳刨機和文本操縱語言都可以讓人難以置信地強大和用途廣泛。你可以很快把某樣東西加工成形、制作接頭、并進行雕刻。如果適當使用,這些工具擁有讓人驚訝的精微與巧妙。但你需要花時間才能掌握它們。   好的文本操縱語言的數目正在增長。Unix開發者常常喜歡利用他們的命令shell的力量,并用像awk和sed這樣的工具加以增強。偏愛更為結構化的工具的人喜歡Python[URL 9]的面向對象本質。有人把Tcl[URL 23]當作自己的首選工具。我們碰巧喜歡用Perl[URL 8]編寫短小的腳本。   這些語言是能賦予你能力的重要技術。使用它們,你可以快速地構建實用程序,為你的想法建立原型——使用傳統語言,這些工作可能需要5倍或10倍的時間。對于我們所做的實驗,這樣的放大系數十分重要。與花費5小時相比,花費30分鐘試驗一個瘋狂的想法要好得多。花費1天使項目的重要組件自動化是可以接受的;花費1周卻不一定。在The Practice of Programming[KP99]一書中,Kernighan與Pike用5種不同的語言構建同一個程序。Perl版本是最短的(17行,而C要150行)。通過Perl你可以操縱文本、與程序交互、進行網絡通信、驅動網頁、進行任意精度的運算、以及編寫看起來像史努比發誓的程序。 提示28: Learn a Text Manipulation Language 學習一種文本操縱語言   為了說明文本操縱語言的廣泛適用性,這里列出了我們過去幾年開發的一些應用示例: l 數據庫schema維護。一組Perl腳本讀取含有數據庫schema定義的純文本文件,根據它生成: - 用于創建數據庫的SQL語句 - 用于填充數據詞典的平板(flat)數據文件 - 用于訪問數據庫的C代碼庫 - 用于檢查數據庫完整性的腳本 - 含有schema描述及框圖的網頁 - schema的XML版本 l Java屬性(property)訪問。限制對某個對象的屬性的訪問,迫使外部類通過方法獲取和設置它們,這是一種良好的OO編程風格。但是,屬性在類的內部由簡單的成員變量表示是一種常見情況,在這樣的情況下要為每個變量創建獲取和設置方法既乏味,又機械。我們有一個Perl腳本,它修改源文件,為所有做了適當標記的變量插入正確的方法定義。 l 測試數據生成。我們的測試數據有好幾萬記錄,散布在若干不同的文件中,其格式也不同,它們需要匯合在一起,并轉換為適于裝載進關系數據庫的某種形式。Perl用幾小時就完成了這一工作(在此過程中還發現了初始數據的幾處一致性錯誤)。 l 寫書。我們認為,出現在書籍中的任何代碼都應首先進行測試,這十分重要。本書中的大多數代碼都經過了測試。但是,按照DRY原則(參見“重復的危害”,26頁),我們不想把代碼從測試過的程序拷貝并粘貼到書里。那意味著代碼是重復的,實際上我們肯定會在程序被改動時忘記更新相應的例子。對于有些例子,我們也不想用編譯并運行例子所需的全部框架代碼來煩擾你。我們轉向了Perl。在我們對書進行格式化時,會調用一個相對簡單的腳本——它提取源文件中指定的片段,進行語法突顯,并把結果轉換成我們使用的排版語言。 l C與Object Pascal的接口。某個客戶有一個在PC上編寫Object Pascal應用的開發團隊。他們的代碼需要與用C編寫的一段代碼接口。我們開發了一個短小的Perl腳本,解析C頭文件,提取所有被導出函數的定義,以及它們使用的數據結構。隨后我們生成Object Pascal單元:用Pascal記錄對應所有的C結構,用導入的過程定義對應所有的C函數。這一生成過程變成了構建的一部分,這樣無論何時C頭文件發生變化,新的Object Pascal單元都會自動被構造。 l 生成Web文檔。許多項目團隊都把文檔發布在內部網站上。我們編寫了許多Perl程序,分析數據庫schema、C或C++源文件、makefile以及其他項目資源,以生成所需的HTML文檔。我們還使用Perl,把文檔用標準的頁眉和頁腳包裝起來,并把它們傳輸到網站上。   我們幾乎每天都使用文本操縱語言。與我們注意到的其他任何語言相比,本書中的許多想法都可以用這些語言更簡單地實現。這些語言使我們能夠輕松地編寫代碼生成器,我們將在下一節討論這一主題。 相關內容: l 重復的危害,26頁 練習: 11. 你的C程序使用枚舉類型表示100種狀態。為進行調試,你想要能把狀態打印成(與數字對應的)字符串。編寫一個腳本,從標準輸入讀取含有以下內容的文件:  (解答在285頁) name state_a state_b : :   生成文件name.h,其中含有: ~~~ extern const char* NAME_names[]; typedef enum { state_a, state_b, : : } NAME;   以及文件name.c,其中含有: const char* NAME_names[] = { "state_a", "state_b", : : }; ~~~ 12. 在本書撰寫的中途,我們意識到我們沒有把use strict指示放進我們的許多Perl例子。編寫一個腳本,檢查某個目錄中的.pl文件,給沒有use strict指示的所有文件在初始注釋塊的末尾加上該指示。要記住給你改動的所有文件保留備份。  (解答在286頁) 20 代碼生成器   當木匠面臨一再地重復制作同一樣東西的任務時,他們會取巧。他們給自己建造夾具或模板。一旦他們做好了夾具,他們就可以反復制作某樣工件。夾具帶走了復雜性,降低了出錯的機會,從而讓工匠能夠自由地專注于質量問題。   作為程序員,我們常常發現自己也處在同樣的位置上。我們需要獲得同一種功能,但卻是在不同的語境中。我們需要在不同的地方重復信息。有時我們只是需要通過減少重復的打字,使自己免于患上腕部勞損綜合癥。   以與木匠在夾具上投入時間相同的方式,程序員可以構建代碼生成器。一旦構建好,在整個項目生命期內都可以使用它,實際上沒有任何代價。 提示29: Write Code That Writes Code 編寫能編寫代碼的代碼   代碼生成器有兩種主要類型: 1. 被動代碼生成器只運行一次來生成結果。然后結果就變成了獨立的——它與代碼生成器分離了。在198頁的邪惡的向導中討論的向導,還有某些CASE工具,都是被動代碼生成器的例子。 2. 主動代碼生成器在每次需要其結果時被使用。結果是用過就扔的——它總是能由代碼生成器重新生成。主動代碼生成器為了生成其結果,常常要讀取某種形式的腳本或控制文件。 被動代碼生成器   被動代碼生成器減少敲鍵次數。它們本質上是參數化模板,根據一組輸入生成給定的輸出形式。結果一經產生,就變成了項目中有充分資格的源文件;它將像任何其他文件一樣被編輯、編譯、置于源碼控制之下。其來源將被忘記。   被動代碼生成器有許多用途: l 創建新的源文件。被動代碼生成器可以生成模板、源碼控制指示、版權說明以及項目中每個新文件的標準注釋塊。我們設置我們的編輯器,讓它在我們每次創建新文件時做這樣的工作:編輯新的Java程序,新的編輯器緩沖區將自動包含注釋塊、包指示以及已經填好的概要的類聲明。 l 在編程語言之間進行一次性轉換。我們開始撰寫本書時使用的是troff系統,但我們在完成了15節以后轉向了LaTeX。我們編寫了一個代碼生成器,讀取troff源,并將其轉換到LaTeX。其準確率大約是90%,余下部分我們用手工完成。這是被動代碼生成器的一個有趣的特性:它們不必完全準確。你需要在你投入生成器的努力和你花在修正其輸出上的精力之間進行權衡。 l 生成查找表及其他在運行時計算很昂貴的資源。許多早期的圖形系統都使用預先計算的正弦和余弦值表,而不是在運行時計算三角函數。在典型情況下,這些表由被動代碼生成器生成,然后拷貝到源文件中。 主動代碼生成器   被動代碼生成器只是一種便利手段,如果你想要遵循DRY原則,它們的“表親”主動代碼生成器卻是必需品。通過主動代碼生成器,你可以取某項知識的一種表示形式,將其轉換為你的應用需要的所有形式。這不是重復,因為衍生出的形式可以用過就扔,并且是由代碼生成器按需生成的(所以才會用主動這個詞)。   無論何時你發現自己在設法讓兩種完全不同的環境一起工作,你都應該考慮使用主動代碼生成器。   或許你在開發數據庫應用。這里,你在處理兩種環境——數據庫和你用來訪問它的編程語言。你有一個schema,你需要定義低級的結構,反映特定的數據庫表的布局。你當然可以直接對其進行編碼,但這違反了DRY原則:schema的知識就會在兩個地方表示。當schema變化時,你需要記住改變相應的代碼。如果某一列從表中被移走,而代碼庫卻沒有改變,甚至有可能連編譯錯誤也沒有。只有等你的測試開始失敗時(或是用戶打電話過來),你才會知道它。   另一種辦法是使用主動代碼生成器——如圖3.3所示,讀取schema,使用它生成結構的源碼。現在,無論何時schema發生變化,用于訪問它的代碼也會自動變化。如果某一列被移走,那么它在結構中相應的字段也將消失,任何使用該列的更高級的代碼就將無法通過編譯。 圖3.3 主動代碼生成器根據數據庫schema創建代碼 你在編譯時就能抓住錯誤,不用等到投入實際運行時。當然,只有在你讓代碼生成成為構建過程自身的一部分的情況下,這個方案才能工作。   使用代碼生成器融合環境的另一個例子發生在不同的編程語言被用于同一個應用時。為了進行通信,每個代碼庫將需要某些公共信息——例如,數據結構、消息格式、以及字段名。要使用代碼生成器,而不是重復這些信息。有時你可以從一種語言的源文件中解析出信息,并將其用于生成第二種語言的代碼。但如下一頁的圖3.4所示,用更簡單、語言中立的表示形式來表示它,并為兩種語言生成代碼,常常更簡單。再看一看268頁上練習13的解答,里面有怎樣把對平板文件表示的解析與代碼生成分離開來的例子。 代碼生成不一定要很復雜   所有這些關于“主動這個”和“被動那個”的談論可能會給你留下這樣的印象:代碼生成器是復雜的東西。它們不一定要很復雜。最復雜的部分通常是負責分析輸入文件的解析器。讓輸入格式保持簡單,代碼生成器就會變得簡單。看一看練習13的解答(286頁):實際的代碼生成基本上是print語句。 圖3.4 根據語言中立的表示生成代碼。在輸入文件中,以‘M’開始的行標志著消息定義的開始。‘F’行定義字段,‘E’是消息的結束 代碼生成器不一定要生成代碼   盡管本節的許多例子給出的是生成程序源碼的代碼生成器,事情并不是非如此不可。你可以用代碼生成器生成幾乎任何輸出:HTML、XML、純文本——可能成為你的項目中別處輸入的任何文本。 相關內容: l 重復的危害,26頁 l 純文本的力量,73頁 l 邪惡的向導,198頁 l 無處不在的自動化,230頁 練習: 13. 編寫一個代碼生成器,讀取圖3.4中的輸入文件,以你選擇的兩種語言生成輸出。設法使它容易增加新語言。  (解答在286頁) 21 按合約設計(1) 提示30: You Can’t Write Perfect Software 你不可能寫出完美的軟件   這刺痛了你?不應該。把它視為生活的公理,接受它,擁抱它,慶祝它。因為完美的軟件不存在。在計算技術簡短的歷史中,沒有一個人曾經寫出過一個完美的軟件。你也不大可能成為第一個。除非你把這作為事實接受下來,否則你最終會把時間和精力浪費在追逐不可能實現的夢想上。   那么,給定了這個讓人壓抑的現實,注重實效的程序員怎樣把它轉變為有利條件?這正是這一章的話題。   每個人都知道只有他們自己是地球上的好司機。所有其他的人都等在那里要對他們不利,這些人亂沖停車標志、在車道之間搖來擺去、不作出轉向指示、打電話、看報紙、總而言之就是不符合我們的標準。于是我們防衛性地開車。我們在麻煩發生之前小心謹慎、預判意外之事、從不讓自己陷入無法解救自己的境地。   編碼的相似性相當明顯。我們不斷地與他人的代碼接合——可能不符合我們的高標準的代碼——并處理可能有效、也可能無效的輸入。所以我們被教導說,要防衛性地編碼。如果有任何疑問,我們就會驗證給予我們的所有信息。我們使用斷言檢測壞數據。我們檢查一致性,在數據庫的列上施加約束,而且通常對自己感到相當滿意。   但注重實效的程序員會更進一步。他們連自己也不信任。知道沒有人能編寫完美的代碼,包括自己,所以注重實效的程序員針對自己的錯誤進行防衛性的編碼。我們將在“按合約設計(Design by Contract)”中描述第一種防衛措施:客戶與供應者必須就權利與責任達成共識。   在“死程序不說謊”中,我們想要確保在找出bug的過程中,不會造成任何破壞。所以我們設法經常檢查各種事項,并在程序出問題時終止程序。   “斷言式編程”描述了一種沿途進行檢查的輕松方法——編寫主動校驗你的假定的代碼。   與其他任何技術一樣,異常如果沒有得到適當使用,造成的危害可能比帶來的好處更多。我們將在“何時使用異常”中討論各種相關問題。   隨著你的程序變得更為動態,你會發現自己在用系統資源玩雜耍——內存、文件、設備,等等。在“怎樣配平資源(How to Balance Resources)”中,我們將提出一些方法,確保你不會讓其中任何一個球掉落下來。   不完美的系統、荒謬的時間標度、可笑的工具、還有不可能實現的需求——在這樣一個世界上,讓我們安全“駕駛”。 當每個人都確實要對你不利時,偏執就是一個好主意。   ——Woody Allen 21 按合約設計 沒有什么比常識和坦率更讓人感到驚訝。   ——拉爾夫·沃爾多·愛默生,《散文集》   與計算機系統打交道很困難。與人打交道更困難。但作為一個族類,我們花費在弄清楚人們交往的問題上的時間更長。在過去幾千年中我們得出的一些解決辦法也可應用于編寫軟件。確保坦率的最佳方案之一就是合約。   合約既規定你的權利與責任,也規定對方的權利與責任。此外,還有關于任何一方沒有遵守合約的后果的約定。   或許你有一份雇用合約,規定了你的工作時數和你必須遵循的行為準則。作為回報,公司付給你薪水和其他津貼。雙方都履行其義務,每個人都從中受益。   全世界都——正式地或非正式地——采用這種理念幫助人們交往。我們能否采用同樣的概念幫助軟件模塊進行交互?答案是肯定的。 DBC   Bertrand Meyer[Mey97b]為Eiffel語言發展了按合約設計的概念[25]。這是一種簡單而強大的技術,它關注的是用文檔記載(并約定)軟件模塊的權利與責任,以確保程序正確性。什么是正確的程序?不多不少,做它聲明要做的事情的程序。用文檔記載這樣的聲明,并進行校驗,是按合約設計(簡稱DBC)的核心所在。   軟件系統中的每一個函數和方法都會做某件事情。在開始做某事之前,例程對世界的狀態可能有某種期望,并且也可能有能力陳述系統結束時的狀態。Meyer這樣描述這些期望和陳述: l 前條件(precondition)。為了調用例程,必須為真的條件;例程的需求。在其前條件被違反時,例程決不應被調用。傳遞好數據是調用者的責任(見115頁的方框)。 l 后條件(postcondition)。例程保證會做的事情,例程完成時世界的狀態。例程有后條件這一事實意味著它會結束:不允許有無限循環。 l 類不變項(class invariant)。類確保從調用者的視角來看,該條件總是為真。在例程的內部處理過程中,不變項不一定會保持,但在例程退出、控制返回到調用者時,不變項必須為真(注意,類不能給出無限制的對參與不變項的任何數據成員的寫訪問)。   讓我們來看一個例程的合約,它把數據值插入惟一、有序的列表中。在iContract(用于Java的預處理器,可從[URL 17]獲取)中,你可以這樣指定: /** * @invariant forall Node n in elements() | * ev() != null * implies * lue().compare To(ev().value()) > 0 */ public class dbc_list { /** * @pre contains(aNode) == false * @post contains(aNode) == true */ public void insertNode(final Node aNode) { // ...   這里我們所說的是,這個列表中的節點必須以升序排列。當你插入新節點時,它不能是已經存在的,我們還保證,在你插入某個節點后,你將能夠找到它。   你用目標編程語言(或許還有某些擴展)編寫這些前條件、后條件以及不變項。例如,除了普通的Java構造體,iContract還提供了謂詞邏輯操作符——forall、exists、還有implies。你的斷言可以查詢方法能夠訪問的任何對象的狀態,但要確保查詢沒有任何副作用(參見124頁)。 DBC與常量參數   后條件常常要使用傳入方法的參數來校驗正確的行為。但如果允許例程改變傳入的參數,你就有可能規避合約。Eiffel不允許這樣的事情發生,但Java卻允許。這里,我們使用Java關鍵字final指示我們的意圖:參數在方法內不應被改變。這并非十分安全——子類有把參數重新聲明為非final的自由。另外,你可以使用iContract語法variable@pre獲取變量在進入方法時的初始值。   這樣,例程與任何潛在的調用者之間的合約可解讀為: 如果調用者滿足了例程的所有前條件,例程應該保證在其完成時、所有后條件和不變項將為真。   如果任何一方沒有履行合約的條款,(先前約定的)某種補償措施就會啟用——例如,引發異常或是終止程序。不管發生什么,不要誤以為沒能履行合約是bug。它不是某種決不應該發生的事情,這也就是為什么前條件不應被用于完成像用戶輸入驗證這樣的任務的原因。 提示31: Design with Contracts 通過合約進行設計   在“正交性”(34頁)中,我們建議編寫“羞怯”的代碼。這里,強調的重點是在“懶惰”的代碼上:對在開始之前接受的東西要嚴格,而允諾返回的東西要盡可能少。記住,如果你的合約表明你將接受任何東西,并允諾返回整個世界,那你就有大量代碼要寫了!   繼承和多態是面向對象語言的基石,是合約可以真正閃耀的領域。假定你正在使用繼承創建“是一種(is-a-kind-of)”關系,即一個類是另外一個類的“一種”。你或許會想要堅持Liskov替換原則(Lis88): 子類必須要能通過基類的接口使用,而使用者無須知道其區別。   換句話說,你想要確保你創建的新子類型確實是基類型的“一種”——它支持同樣的方法,這些方法有同樣的含義。我們可以通過合約來做到這一點。要讓合約自動應用于將來的每個子類,我們只須在基類中規定合約一次。子類可以(可選地)接受范圍更廣的輸入,或是作出更強的保證。但它所接受的和所保證的至少與其父類一樣多。   例如,考慮Java基類t.Component。你可以把AWT或Swing中的任何可視組件當作Component,而不用知道實際的子類是按鈕、畫布、菜單,還是別的什么。每個個別的組件都可以提供額外的、特殊的功能,但它必須至少提供Component定義的基本能力。但并沒有什么能阻止你創建Component的一個子類型,提供名稱正確、但所做事情卻不正確的方法。你可以很容易地創建不進行繪制的paint方法,或是不設置字體的setFont方法。AWT沒有用于抓住你沒有履行合約的事實的合約。   沒有合約,編譯器所能做的只是確保子類符合特定的方法型構(signature)。但如果我們適當設定基類合約,我們現在就能夠確保將來任何子類都無法改變我們的方法的含義。例如,你可能想要這樣為setFont建立合約,確保你設置的字體就是你得到的字體: /** * @pre f != null * @post getFont() == f */ public void setFont(final Font f) { // ... 21 按合約設計(2) 實現DBC   使用DBC的最大好處也許是它迫使需求與保證的問題走到前臺來。在設計時簡單地列舉輸入域的范圍是什么、邊界條件是什么、例程允諾交付什么——或者,更重要的,它不允諾交付什么——是向著編寫更好的軟件的一次飛躍。不對這些事項作出陳述,你就回到了靠巧合編程(參見172頁),那是許多項目開始、結束、失敗的地方。   如果語言不在代碼中支持DBC,你也許就只能走這么遠了——這并不太壞。畢竟,DBC是一種設計技術。即使沒有自動檢查,你也可以把合約作為注釋放在代碼中,并仍然能夠得到非常實際的好處。至少,在遇到麻煩時,用注釋表示的合約給了你一個著手的地方。 斷言   盡管用文檔記載這些假定是一個了不起的開始,讓編譯器為你檢查你的合約,你能夠獲得大得多的好處。在有些語言中,你可以通過斷言(參見斷言式編程,122頁)對此進行部分的模擬。為何只是部分的?你不能用斷言做DBC能做的每一件事情嗎?   遺憾的是,答案是“不能”。首先,斷言不能沿著繼承層次向下遺傳。這就意味著,如果你重新定義了某個具有合約的基類方法,實現該合約的斷言不會被正確調用(除非你在新代碼中手工復制它們)。在退出每個方法之前,你必須記得手工調用類不變項(以及所有的基類不變項)。根本的問題是合約不會自動實施。   還有,不存在內建的“老”值概念。也就是,與存在于方法入口處的值相同的值。如果你使用斷言實施合約,你必須給前條件增加代碼,保存你想要在后條件中使用的任何信息。把它與iContract比較一下,其后條件可以引用“variable@pre”;或者與Eiffel比較一下,它支持“老表達式”。   最后,runtime系統和庫的設計不支持合約,所以它們的調用不會被檢查。這是一個很大的損失,因為大多數問題常常是在你的代碼和它使用的庫之間的邊界上檢測到的(更詳細的討論,參見死程序不說謊,120頁)。 語言支持   有內建的DBC支持的語言(比如Eiffel和Sather[URL 12])自動在編譯器和runtime系統中檢查前條件和后條件。在這樣的情況下,你能獲得最大的好處,因為所有的代碼庫(還有庫函數)必須遵守它們的合約。   但像C、C++和Java這樣的更流行的語言呢?對于這些語言,有一些預處理器能夠處理作為特殊注釋嵌入在原始源碼中的合約。預處理器會把這些注釋展開成檢驗斷言的代碼。   對于C和C++,你可以研究一下Nana[URL 18]。Nana不處理繼承,但它卻能以一種新穎的方式、使用調試器在運行時監控斷言。   對于Java,可以使用iContract[URL 17]。它讀取(JavaDoc形式的)注釋,生成新的包含了斷言邏輯的源文件。   預處理器沒有內建設施那么好。把它們集成進你的項目可能會很雜亂,而且你使用的其他庫沒有合約。但它們仍然很有助益;當某個問題以這樣的方式被發現時——特別是你本來決不會發現的問題——那幾乎像是魔術。 DBC與早崩潰   DBC相當符合我們關于早崩潰的概念(參見“死程序不說謊”,120頁)。假定你有一個計算平方根的方法(比如在Eiffel的DOUBLE類中)。它需要一個前條件,把參數域限制為正數。Eiffel的前條件通過關鍵字require聲明,后條件通過ensure聲明,所以你可以編寫: sqrt: DOUBLE is -- Square root routine require sqrt_arg_must_be_positive: Current >= 0; --- ... --- calculate square root here --- ... ensure ((Result*Result) - Current).abs <= epsilon*Cs; -- Result should be within error tolerance end; 誰負責?   誰負責檢查前條件,是調用者,還是被調用的例程?如果作為語言的一部分實現,答案是兩者都不是:前條件是在調用者調用例程之后,但在進入例程自身之前,在幕后測試的。因而如果要對參數進行任何顯式的檢查,就必須由調用者來完成,因為例程自身永遠也不會看到違反了其前條件的參數。(對于沒有內建支持的語言,你需要用檢查這些斷言的“前言”(preamble)和/或“后文”(postamble)把被調用的例程括起來)   考慮一個程序,它從控制臺讀取數字,(通過調用sqrt)計算其平方根,并打印結果。sqrt函數有一個前條件——其參數不能為負。如果用戶在控制臺上輸入負數,要由調用代碼確保它不會被傳給sqrt。該調用代碼有許多選擇:它可以終止,可以發出警告并讀取另外的數,也可以把這個數變成正數,并在sqrt返回的結果后面附加一個“i”。無論其選擇是什么,這都肯定不是sqrt的問題。   通過在sqrt例程的前條件中表示平方根函數的參數域,你把保證正確性的負擔轉交給了調用者——本應如此。隨后你可以在知道了其輸入會落在有效范圍內的前提下,安全地設計sqrt例程。   如果你用于計算平方根的算法失敗了(或不在規定的錯誤容忍程度之內),你會得到一條錯誤消息,以及用于告訴你調用鏈的棧蹤跡(stack trace)。   如果你傳給sqrt一個負參數,Eiffel runtime會打印錯誤“sqrt_arg_must_be_positive”,還有棧蹤跡。這比像Java、C和C++等語言中的情況要好,在這些語言那里,把負數傳給sqrt,返回的是特殊值NaN(Not a Number)。要等到你隨后在程序中試圖對NaN進行某種運算時,你才會得到讓你吃驚的結果。   通過早崩潰、在問題現場找到和診斷問題要容易得多。 不變項的其他用法   到目前為止,我們已經討論了適用于單個方法的前條件和后條件,以及應用于類中所有方法的不變項,但使用不變項還有其他一些有用的方式。 循環不變項   在復雜的循環上正確設定邊界條件可能會很成問題。循環常有香蕉問題(我知道怎樣拼寫“banana”,但不知道何時停下來——“bananana…”)、籬笆樁錯誤(不知道該數樁還是該數空)、以及無處不在的“差一個”錯誤[URL 52]。   在這些情況下,不變項可以有幫助:循環不變項是對循環的最終目標的陳述,但又進行了一般化,這樣在循環執行之前和每次循環迭代時,它都是有效的。你可以把它視為一種微型合約。經典的例子是找出數組中的最大值的例程: int m = arr[0]; // example assumes ngth > 0 int i = 1; // Loop invariant: m = max(arr[0:i-1]) while (i < ngth) { m = Mx(m, arr[i]); i = i + 1; }   (arr[m:n]是便捷表示法,意為數組從下標m到n的部分。)不變項在循環運行之前必須為真,循環的主體必須確保它在循環執行時保持為真。這樣我們就知道不變項在循環終止時也保持不變,因而我們的結果是有效的。循環不變項可被顯式地編寫成斷言,但作為設計和文檔工具,它們也很有用。 語義不變項   你可以使用語義不變項(semantic invariant)表達不可違反的需求,一種“哲學合約”。   我們曾經編寫過一個借記卡交易交換程序。一個主要的需求是借記卡用戶的同一筆交易不能被兩次記錄到賬戶中。換句話說,不管發生何種方式的失敗,結果都應該是:不處理交易,而不是處理重復的交易。   這個簡單的法則,直接由需求驅動,被證明非常有助于處理復雜的錯誤恢復情況,并且可以在許多領域中指導詳細的設計和實現。   一定不要把固定的需求、不可違反的法則與那些僅僅是政策(policiy)的東西混為一談,后者可能會隨著新的管理制度的出臺而改變。這就是我們為什么要使用術語“語義不變項”的原因——它必須是事物的確切含義的中心,而不受反復無常的政策的支配(后者是更為動態的商業規則的用途所在)。   當你發現合格的需求時,確保讓它成為你制作的無論什么文檔的一個眾所周知的部分——無論它是一式三份簽署的需求文檔中的圓點列表,還是只是每個人都能看到的公共白板上的重要通知。設法清晰、無歧義地陳述它。例如,在借記卡的例子中,我們可以寫: 出錯時要偏向消費者   這是清楚、簡潔、無歧義的陳述,適用于系統的許多不同的區域。它是我們與系統的所有用戶之間的合約,是我們對行為的保證。 動態合約與代理   直到現在為止,我們一直把合約作為固定的、不可改變的規范加以談論。但在自治代理(autonomous agent)的領域中,情況并不一定是這樣。按照“自治”的定義,代理有拒絕它們不想接受的請求的自由——“我無法提供那個,但如果你給我這個,那么我可以提供另外的某樣東西。”   無疑,任何依賴于代理技術的系統對合約協商的依賴都是至關緊要的——即使它們是動態生成的。   設想一下,通過足夠的“能夠互相磋商合約、以實現某個目標”的組件和代理,我們也許就能解決軟件生產率危機:讓軟件為我們解決它。   但如果我們不能手工使用合約,我們也無法自動使用它們。所以下次你設計軟件時,也要設計它的合約。 相關內容: l 正交性,34頁 l 死程序不說謊,120頁 l 斷言式編程,122頁 l 怎樣配平資源,129頁 l 解耦與得墨忒耳法則,138頁 l 時間耦合,150頁 l 靠巧合編程,172頁 l 易于測試的代碼,189頁 l 注重實效的團隊,224頁 挑戰: l 思考這樣的問題:如果DBC如此強大,它為何沒有得到更廣泛的使用?制定合約困難嗎?它是否會讓你思考你本來想先放在一邊的問題?它迫使你思考嗎?顯然,這是一個危險的工具! 練習: 14. 好合約有什么特征?任何人都可以增加前條件和后條件,但那是否會給你帶來任何好處?更糟糕的是,它們實際上帶來的壞處是否會大過好處?對于下面的以及練習15和16中的例子,確定所規定的合約是好、是壞、還是很糟糕,并解釋為什么。   首先,讓我們看一個Eiffel例子。我們有一個用于把STRING添加到雙向鏈接的循環鏈表中的例程(別忘了前條件用require標注,后條件用ensure標注)。  (解答在288頁) -- Add an item to a doubly linked list, -- and return the newly created NODE. add_item (item : STRING) : NODE is require item /= Void -- '/=' is 'not equal'. deferred -- Abstract base class. ensure evious = result -- Check the newly xt = result -- added node's links. find_item(item) = result -- Should find it. End 15. 下面,讓我們試一試一個Java的例子——與練習14中的例子有點類似。insertNumber把整數插入有序列表中。前條件和后條件的標注方式與iContract(參見[URL 17])一樣。 (解答在288頁) private int data[]; /** * @post data[index-1] < data[index] && * data[index] == aValue */ public Node insertNumber (final int aValue) { int index = findPlaceToInsert(aValue); ... 16. 下面的代碼段來自Java的棧類。這是好合約嗎?  (解答在289頁) /** * @pre anItem != null // Require real data * @post pop() == anItem // Verify that it's * // on the stack */ public void push(final String anItem) 17. DBC的經典例子(如練習14-16中的例子)給出的是某種ADT(Abstract Data Type)的實現——棧或隊列就是典型的例子。但并沒有多少人真的會編寫這種低級的類。   所以,這個練習的題目是,設計一個廚用攪拌機接口。它最終將是一個基于Web、適用于Internet、CORBA化的攪拌機,但現在我們只需要一個接口來控制它。它有十擋速率設置(0表示關機)。你不能在它空的時候進行操作,而且你只能一擋一擋地改變速率(也就是說,可以從0到1,從1到2,但不能從0到2)。   下面是各個方法。增加適當的前條件、后條件和不變項。 (解答在289頁) int getSpeed() void setSpeed(int x) boolean isFull() void fill() void empty() 18. 在0, 5, 10, 15, …,100序列中有多少個數?  (解答在290頁) 22 死程序不說謊   你是否注意到,有時別人在你自己意識到之前就能覺察到你的事情出了問題。別人的代碼也是一樣。如果我們的某個程序開始出錯,有時庫例程會最先抓住它。一個“迷途的”指針也許已經致使我們用無意義的內容覆寫了某個文件句柄。對read的下一次調用將會抓住它。或許緩沖區越界已經把我們要用于檢測分配多少內存的計數器變成了垃圾。也許我們對malloc的調用將會失敗。數百萬條之前的某個邏輯錯誤意味著某個case語句的選擇開關不再是預期的1、2或3。我們將會命中default情況(這是為什么每個case/switch語句都需要有default子句的原因之一——我們想要知道何時發生了“不可能”的事情)。   我們很容易掉進“它不可能發生”這樣一種心理狀態。我們中的大多數人編寫的代碼都不檢查文件是否能成功關閉,或者某個跟蹤語句是否已按照我們的預期寫出。而如果所有的事情都能如我們所愿,我們很可能就不需要那么做——這些代碼在任何正常的條件都不會失敗。但我們是在防衛性地編程,我們在程序的其他部分中查找破壞堆棧的“淘氣指針”,我們在檢查確實加載了共享庫的正確版本。   所有的錯誤都能為你提供信息。你可以讓自己相信錯誤不可能發生,并選擇忽略它。但與此相反,注重實效的程序員告訴自己,如果有一個錯誤,就說明非常、非常糟糕的事情已經發生了。 提示32: Crash Early 早崩潰 要崩潰,不要破壞(trash)   盡早檢測問題的好處之一是你可以更早崩潰。而有許多時候,讓你的程序崩潰是你的最佳選擇。其他的辦法可以是繼續執行、把壞數據寫到某個極其重要的數據庫或是命令洗衣機進入其第二十次連續的轉動周期。   Java語言和庫已經采用了這一哲學。當意料之外的某件事情在runtime系統中發生時,它會拋出RuntimeException。如果沒有被捕捉,這個異常就會滲透到程序的頂部,致使其中止,并顯示棧蹤跡。   你可以在別的語言中做相同的事情。如果沒有異常機制,或是你的庫不拋出異常,那么就確保你自己對錯誤進行了處理。在C語言中,對于這一目的,宏可能非常有用: #define CHECK(LINE, EXPECTED) \ { int rc = LINE; \ if (rc != EXPECTED) \ ut_abort(__FILE__, __LINE__, #LINE, rc, EXPECTED); } void ut_abort(char *file, int ln, char *line, int rc, int exp) { fprintf(stderr, "%s line %d\n'%s': expected %d, got %d\n", file, ln, line, exp, rc); exit(1); }   然后你可以這樣包裝決不應該失敗的調用: CHECK(stat("/tmp", &stat_buff), 0);   如果它失敗了,你就會得到寫到stderr的消息: source.c line 19 'stat("/tmp", &stat_buff)': expected 0, got -1   顯然,有時簡單地退出運行中的程序并不合適。你申請的資源可能沒有釋放,或者你可能要寫出日志消息,清理打開的事務,或與其他進程交互。我們在“何時使用異常”(125頁)中討論的技術在此能對你有幫助。但是,基本的原則是一樣的——當你的代碼發現,某件被認為不可能發生的事情已經發生時,你的程序就不再有存活能力。從此時開始,它所做的任何事情都會變得可疑,所以要盡快終止它。死程序帶來的危害通常比有疾患的程序要小得多。 相關內容: l 按合約設計,109頁 l 何時使用異常,125頁 23 斷言式編程 在自責中有一種滿足感。當我們責備自己時,會覺得再沒人有權責備我們。   ——奧斯卡?王爾德:《多里安?格雷的畫像》   每一個程序員似乎都必須在其職業生涯的早期記住一段曼特羅(mantra)。它是計算技術的基本原則,是我們學著應用于需求、設計、代碼、注釋——也就是我們所做的每一件事情——的核心信仰。那就是: 這決不會發生……   “這些代碼不會被用上30年,所以用兩位數字表示日期沒問題。”“這個應用決不會在國外使用,那么為什么要使其國際化?”“count不可能為負。”“這個printf不可能失敗。”   我們不要這樣自我欺騙,特別是在編碼時。 提示33: If It Can’t Happen, Use Assertions to Ensure That It Won’t 如果它不可能發生,用斷言確保它不會發生   無論何時你發現自己在思考“但那當然不可能發生”,增加代碼檢查它。最容易的辦法是使用斷言。在大多數C和C++實現中,你都能找到某種形式的檢查布爾條件的assert或_assert宏。這些宏是無價的財富。如果傳入你的過程的指針決不應該是NULL,那么就檢查它: void writeString(char *string) { assert(string != NULL); ...   對于算法的操作,斷言也是有用的檢查。也許你編寫了一個聰明的排序算法。檢查它是否能工作: for (int i = 0; i < num_entries-1; i++) { assert(sorted[i] <= sorted[i+1]); }   當然,傳給斷言的條件不應該有副作用(參見124頁的方框)。還要記住斷言可能會在編譯時被關閉——決不要把必須執行的代碼放在assert中。   不要用斷言代替真正的錯誤處理。斷言檢查的是決不應該發生的事情:你不會想編寫這樣的代碼: printf("Enter 'Y' or 'N': "); ch = getchar(); assert((ch == 'Y') || (ch == 'N')); /* bad idea! */   而且,提供給你的assert宏會在斷言失敗時調用exit,并不意味著你編寫的版本就應該這么做。如果你需要釋放資源,就讓斷言失敗生成異常、longjump到某個退出點、或是調用錯誤處理器。要確保你在終止前的幾毫秒內執行的代碼不依賴最初觸發斷言失敗的信息。 讓斷言開著   有一個由編寫編譯器和語言環境的人傳播的、關于斷言的常見誤解。就是像這樣的說法:   斷言給代碼增加了一些開銷。因為它們檢查的是決不應該發生的事情,所以只會由代碼中的bug觸發。一旦代碼經過了測試并發布出去,它們就不再需要存在,應該被關閉,以使代碼運行得更快。斷言是一種調試設施。   這里有兩個明顯錯誤的假定。首先,他們假定測試能找到所有的bug。現實的情況是,對于任何復雜的程序,你甚至不大可能測試你的代碼執行路徑的排列數的極小一部分(參見“無情的測試”,245頁)。其次,樂觀主義者們忘記了你的程序運行在一個危險的世界上。在測試過程中,老鼠可能不會噬咬通信電纜、某個玩游戲的人不會耗盡內存、日志文件不會塞滿硬盤。這些事情可能會在你的程序運行在實際工作環境中時發生。你的第一條防線是檢查任何可能的錯誤,第二條防線是使用斷言設法檢測你疏漏的錯誤。   在你把程序交付使用時關閉斷言就像是因為你曾經成功過,就不用保護網去走鋼絲。那樣做有極大的價值,但卻難以獲得人身保險。   即使你確實有性能問題,也只關閉那些真的有很大影響的斷言。上面的排序例子 斷言與副作用   如果我們增加的錯誤檢測代碼實際上卻制造了新的錯誤,那是一件讓人尷尬的事情。如果對條件的計算有副作用,這樣的事情可能會在使用斷言時發生。例如,在Java中,像下面這樣編寫代碼,不是個好主意: while (smoreElements () { Test.ASSERT(xtElements() != null); object obj = xtElement(); // .... }   ASSERT中的.nextElement()調用有副作用:它會讓迭代器越過正在讀取的元素,這樣循環就會只處理集合中的一半元素。這樣編寫代碼會更好: while (smoreElements()) { object obj = xtElement(); Test.ASSERT(obj != null); //.... }   這個問題是一種“海森堡蟲子”(Heisenbug)——調試改變了被調試系統的行為(參見[URL 52])。 也許是你的應用的關鍵部分,也許需要很快才行。增加檢查意味著又一次通過數據,這可能讓人不能接受。讓那個檢查成為可選的,但讓其余的留下來。 相關部分: l 調試,90頁 l 按合約設計,109頁 l 怎樣配平資源,129頁 l 靠巧合編程,172頁 練習: 19. 一次快速的真實性檢查。下面這些“不可能”的事情中,那些可能發生?  (解答在290頁) 1. 一個月少于28天 2. stat(“.”, &sb) == -1 (也就是,無法訪問當前目錄) 3. 在C++里:a = 2; b = 3; if (a + b != 5) exit(1); 4. 內角和不等于180°的三角形。 5. 沒有60秒的一分鐘 6. 在Java中:(a + 1) <= a 20. 為Java開發一個簡單的斷言檢查類。  (解答在291頁) 24 何時使用異常   在“死程序不說謊”(120頁)中,我們提出,檢查每一個可能的錯誤——特別是意料之外的錯誤——是一種良好的實踐。但是,在實踐中這可能會把我們引向相當丑陋的代碼;你的程序的正常邏輯最后可能會被錯誤處理完全遮蔽,如果你贊成“例程必須有單個return語句”的編程學派(我們不贊成),情況就更是如此。我們見過看上去像這樣的代碼: retcode = OK; if (ad(name) != OK) { retcode = BAD_READ; } else { processName(name); if (ad(address) != OK) { retcode = BAD_READ; } else { processAddress(address); if (ad(telNo) != OK) { retcode = BAD_READ; } else { // etc, etc... } } } return retcode;   幸運的是,如果編程語言支持異常,你可以通過更為簡潔的方式重寫這段代碼: retcode = OK; try { ad(name); process(name); ad(address); processAddress(address); ad(telNo); // etc, etc... } catch (IOException e) { retcode = BAD_READ; Lg("Error reading individual: " + tMessage()); } return retcode;   現在正常的控制流很清晰,所有的錯誤處理都移到了一處。 什么是異常情況   關于異常的問題之一是知道何時使用它們。我們相信,異常很少應作為程序的正常流程的一部分使用;異常應保留給意外事件。假定某個未被抓住的異常會終止你的程序,問問你自己:“如果我移走所有的異常處理器,這些代碼是否仍然能運行?”如果答案是“否”,那么異常也許就正在被用在非異常的情形中。   例如,如果你的代碼試圖打開一個文件進行讀取,而該文件并不存在,應該引發異常嗎?   我們的回答是:“這取決于實際情況。”如果文件應該在那里,那么引發異常就有正當理由。某件意外之事發生了——你期望其存在的文件好像消失了。另一方面,如果你不清楚該文件是否應該存在,那么你找不到它看來就不是異常情況,錯誤返回就是合適的。   讓我們看一看第一種情況的一個例子。下面的代碼打開文件/etc/passwd,這個文件在所有的UNIX系統上都應該存在。如果它失敗了,它會把FileNotFoundException傳給它的調用者。 public void open_passwd() throws FileNotFoundException { // This may throw FileNotFoundException... ipstream = new FileInputStream("/etc/passwd"); // ... }   但是,第二種情況可能涉及打開用戶在命令行上指定的文件。這里引發異常沒有正當理由,代碼看起來也不同: public boolean open_user_file(String name) throws FileNotFoundException { File f = new File(name); if (!ists()) { return false; } ipstream = new FileInputStream(f); return true; }   注意FileInputStream調用仍有可能生成異常,這個例程會把它傳遞出去。但是,這個異常只在真正異常的情形下才生成;只是試圖打開不存在的文件將生成傳統的錯誤返回。 提示34: Use Exceptions for Exceptional Problems 將異常用于異常的問題   我們為何要提出這種使用異常的途徑?嗯,異常表示即時的、非局部的控制轉移——這是一種級聯的(cascading)goto。那些把異常用作其正常處理的一部分的程序,將遭受到經典的意大利面條式代碼的所有可讀性和可維護性問題的折磨。這些程序破壞了封裝:通過異常處理,例程和它們的調用者被更緊密地耦合在一起。 錯誤處理器是另一種選擇   錯誤處理器是檢測到錯誤時調用的例程。你可以登記一個例程處理特定范疇的錯誤。處理器會在其中一種錯誤發生時被調用。   有時你可能想要使用錯誤處理器,或者用于替代異常,或者與異常一起使用。顯然,如果你使用像C這樣不支持異常的語言,這是你的很少幾個選擇之一(參見下一頁的“挑戰”)。但是,有時錯誤處理器甚至也可用于擁有良好的內建異常處理方案的語言(比如Java)。   考慮一個客戶-服務器應用的實現,它使用了Java的Remote Method Invocation(RMI)設施。因為RMI的實現方式,每個對遠地例程的調用都必須準備處理RemoteException。增加代碼處理這些異常可能會變得讓人厭煩,并且意味著我們難以編寫既能與本地例程、也能與遠地例程一起工作的代碼。一種繞開這一問題的可能方法是把你的遠地對象包裝在非遠地的類中。這個類隨即實現一個錯誤處理器接口,允許客戶代碼登記一個在檢測到遠地異常時調用的例程。 相關內容: l 死程序不說謊,120頁 挑戰: l 不支持異常的語言常常擁有一些其他的非局部控制轉移機制(例如,C擁有longjmp/setjmp)。考慮一下怎樣使用這些設施實現某種仿造的異常機制。其好處和危險是什么?你需要采取什么特殊步驟確保資源不被遺棄?在你編寫的所有C代碼中使用這種解決方案有意義嗎? 練習: 21. 在設計一個新的容器類時,你確定可能有以下錯誤情況:  (解答在292頁) (1) add例程中的新元素沒有內存可用 (2) 在fetch例程中找不到所請求的數據項 (3) 傳給add例程的是null指針 應怎樣處理每種情況?應該生成錯誤、引發異常、還是忽略該情況? 25怎樣配平資源 “我把你帶進這個世界,”我的父親會說:“我也可以把你趕出去。那沒有我影響。我要再造另一個你。”   ——Bill Cosby,Fatherhood   只要在編程,我們都要管理資源:內存、事務、線程、文件、定時器——所有數量有限的事物。大多數時候,資源使用遵循一種可預測的模式:你分配資源、使用它,然后解除其分配。   但是,對于資源分配和解除分配的處理,許多開發者沒有始終如一的計劃。所以讓我們提出一個簡單的提示: 提示35: Finish What You Start 要有始有終   在大多數情況下這條提示都很容易應用。它只是意味著,分配某項資源的例程或對象應該負責解除該資源的分配。讓我們通過一個糟糕的代碼例子來看一看該提示的應用方式——這是一個打開文件、從中讀取消費者信息、更新某個字段、然后寫回結果的應用。我們除去了其中的錯誤處理代碼,以讓例子更清晰: void readCustomer(const char *fName, Customer *cRec) { cFile = fopen(fName, "r+"); fread(cRec, sizeof(*cRec), 1, cFile); } void writeCustomer(Customer *cRec) { rewind(cFile); fwrite (cRec, sizeof(*cRec), 1, cFile); fclose(cFile); } void updateCustomer(const char *fName, double newBalance) { Customer cRec; readCustomer(fName, &cRec); cRlance = newBalance; writeCustomer(&cRec); }   初看上去,例程updateCustomer相當好。它似乎實現了我們所需的邏輯——讀取記錄,更新余額,寫回記錄。但是,這樣的整潔掩蓋了一個重大的問題。例程readCustomer和writeCustomer緊密地耦合在一起[27]——它們共享全局變量cFile。readCustomer打開文件,并把文件指針存儲在cFile中,而writeCustomer使用所存儲的指針在其結束時關閉文件。這個全局變量甚至沒有出現在updateCustomer例程中。   這為什么不好?讓我們考慮一下,不走運的維護程序員被告知規范發生了變化——余額只應在新的值不為負時更新。她進入源碼,改動updateCustomer: void updateCustomer(const char *fName, double newBalance) { Customer cRec; readCustomer(fName, &cRec); if (newBalance >= 0.0) { cRlance = newBalance; writeCustomer(&cRec); } }   在測試時一切似乎都很好。但是,當代碼投入實際工作,若干小時后它就崩潰了,抱怨說打開的文件太多。因為writeCustomer在有些情形下不會被調用,文件也就不會被關閉。   這個問題的一個非常糟糕的解決方案是在updateCustomer中對該特殊情況進行處理: void updateCustomer(const char *fName, double newBalance) { Customer cRec; readCustomer(fName, &cRec); if (newBalance >= 0.0) { cRlance = newBalance; writeCustomer(&cRec); } else fclose(cFile); }   這可以修正問題——不管新的余額是多少,文件現在都會被關閉——但這樣的修正意味著三個例程通過全局的cFile耦合在一起。我們在掉進陷阱,如果我們繼續沿著這一方向前進,事情就會開始迅速變糟。   要有始有終這一提示告訴我們,分配資源的例程也應該釋放它。通過稍稍重構代碼,我們可以在此應用該提示: void readCustomer(FILE *cFile, Customer *cRec) { fread(cRec, sizeof(*cRec), 1, cFile); } void writeCustomer(FILE *cFile, Customer *cRec) { rewind(cFile); fwrite(cRec, sizeof(*cRec), 1, cFile); } void updateCustomer(const char *fName, double newBalance) { FILE *cFile; Customer cRec; cFile = fopen(fName, "r+"); // >--- readCustomer(cFile, &cRec); // / if (newBalance >= 0.0) { // / cRlance = newBalance; // / writeCustomer(cFile, &cRec); // / } // / fclose(cFile); // <--- }   現在updateCustomer例程承擔了關于該文件的所有責任。它打開文件并(有始有終地)在退出前關閉它。例程配平了對文件的使用:打開和關閉在同一個地方,而且顯然每一次打開都有對應的關閉。重構還移除了丑陋的全局變量。 嵌套的分配   對于一次需要不只一個資源的例程,可以對資源分配的基本模式進行擴展。有兩個另外的建議: 1. 以與資源分配的次序相反的次序解除資源的分配。這樣,如果一個資源含有對另一個資源的引用,你就不會造成資源被遺棄。 2. 在代碼的不同地方分配同一組資源時,總是以相同的次序分配它們。這將降低發生死鎖的可能性。(如果進程A申請了resource1,并正要申請resource2,而進程B申請了resource2,并試圖獲得resource1,這兩個進程就會永遠等待下去。) 不管我們在使用的是何種資源——事務、內存、文件、線程、窗口——基本的模式都適用: 無論是誰分配的資源,它都應該負責解除該資源的分配。但是,在有些語言中,我們可以進一步發展這個概念。 對象與異常   分配與解除分配的對稱讓人想起類的構造器與析構器。類代表某個資源,構造器給予你該資源類型的特定對象,而析構器將其從你的作用域中移除。   如果你是在用面向對象語言編程,你可能會發現把資源封裝在類中很有用。每次你需要特定的資源類型時,你就實例化這個類的一個對象。當對象出作用域或是被垃圾收集器回收時,對象的析構器就會解除所包裝資源的分配。 配平與異常   支持異常的語言可能會使解除資源的分配很棘手。如果有異常被拋出,你怎樣保證在發生異常之前分配的所有資源都得到清理?答案在一定程度上取決于語言。 在C++異常機制下配平資源   C++支持try…catch異常機制。遺憾的是,這意味著在退出某個捕捉異常、并隨即將其重新拋出的例程時,總是至少有兩條可能的路徑: void doSomething(void) { Node *n = new Node; try { // do something } catch (...) { delete n; throw; } delete n; }   注意我們創建的節點是在兩個地方釋放的——一次是在例程正常的退出路徑上,一次是在異常處理器中。這顯然違反了DRY原則,可能會發生維護問題。   但是,我們可以對C++的語義加以利用。局部對象在從包含它們的塊中退出時會被自動銷毀。這給了我們一些選擇。如果情況允許,我們可以把“n”從指針改變為棧上實際的Node對象: void doSomething1(void) { Node n; try { // do something } catch (...) { throw; } }   在這里,不管是否拋出異常,我們都依靠C++自動處理Node對象的析構。   如果不可能不使用指針,可以通過在另一個類中包裝資源(在這個例子中,資源是一個Node指針)獲得同樣的效果。 // Wrapper class for Node resources class NodeResource { Node *n; public: NodeResource() { n = new Node; } ~NodeResource() { delete n; } Node *operator->() { return n; } }; void doSomething2(void) { NodeResource n; try { // do something } catch (...) { throw; } }   現在包裝類NodeResource確保了在其對象被銷毀時,相應的節點也會被銷毀。為了方便起見,包裝提供了解除引用操作符->,這樣它的使用者可以直接訪問所包含的Node對象中的字段。   因為這一技術是如此有用,標準C++庫提供了模板類auto_ptr,能自動包裝動態分配的對象。 void doSomething3(void) { auto_ptr<Node> p (new Node); // Access the Node as p->... // Node automatically deleted at end } 在Java中配平資源   與C++不同,Java實現的是自動對象析構的一種“懶惰”形式。未被引用的對象被認為是垃圾收集的候選者,如果垃圾收集器回收它們,它們的finalize方法就會被調用。盡管這為開發者提供了便利,他們不再須要為大多數內存泄漏承受指責,但同時也使得實現C++方式的資源清理變得很困難。幸運的是,Java語言的設計者考慮周詳地增加了一種語言特性進行補償:finally子句。當try塊含有finally子句時,如果try塊中有任何語句被執行,該子句中的代碼就保證會被執行。是否有異常拋出沒有影響(即或try塊中的代碼執行了return語句)——finally子句中的代碼都將會運行。這意味著我們可以通過這樣的代碼配平我們的資源使用: public void doSomething() throws IOException { File tmpFile = new File(tmpFileName); FileWriter tmp = new FileWriter(tmpFile); try { // do some work } finally { tmpFlete(); } }   該例程使用了一個臨時文件,不管例程怎樣退出,我們都要刪除該文件。finally塊使得我們能夠簡潔地表達這一意圖。 當你無法配平資源時   有時基本的資源分配模式并不合適。這通常會出現在使用動態數據結構的程序中。一個例程將分配一塊內存區,并把它鏈接進某個更大的數據結構中,這塊內存可能會在那里呆上一段時間。   這里的訣竅是為內存分配設立一個語義不變項。你須要決定誰為某個聚集數據結構(aggregate data structure)中的數據負責。當你解除頂層結構的分配時會發生什么?你有三個主要選擇: 1. 頂層結構還負責釋放它包含的任何子結構。這些結構隨即遞歸地刪除它們包含的數據,等等。 2. 只是解除頂層結構的分配。它指向的(沒有在別處引用的)任何結構都會被遺棄。 3. 如果頂層結構含有任何子結構,它就拒絕解除自身的分配。   這里的選擇取決于每個數據結構自身的情形。但是,對于每個結構,你都須明確做出選擇,并始終如一地實現你的選擇。在像C這樣的過程語言中實現其中的任何選擇都可能會成問題:數據結構自身不是主動的。在這樣的情形下,我們的偏好是為每個重要結構編寫一個模塊,為該結構提供分配和解除分配設施(這個模塊也可以提供像調試打印、序列化、解序列化和遍歷掛鉤這樣的設施)。   最后,如果追蹤資源很棘手,你可以通過在動態分配的對象上實現一種引用計數方案,編寫自己有限的自動垃圾回收機制。More Effective C++[Mey96]一書專設了一節討論這一話題。 檢查配平   因為注重實效的程序員誰也不信任,包括我們自己,所以我們覺得,構建代碼、對資源確實得到了適當釋放進行實際檢查,這總是一個好主意。對于大多數應用,這通常意味著為每種資源類型編寫包裝,并使用這些包裝追蹤所有的分配和解除分配。在你的代碼中的特定地方,程序邏輯將要求資源處在特定的狀態中:使用包裝對此進行檢查。   例如,一個長期運行的、對請求進行服務的程序,很可能會在其主處理循環的頂部的某個地方等待下一個請求到達。這是確定自從上次循環執行以來,資源使用未曾增長的好地方。   在一個更低、但用處并非更少的層面上,你可以投資購買能檢查運行中的程序的內存泄漏情況(及其他情況)的工具。Purify(m)和Insure++(m)是兩種流行的選擇。 相關內容: l 按合約設計,109頁 l 斷言式編程,122頁 l 解耦與得墨忒耳法則,138頁 挑戰: l 盡管沒有什么途徑能夠確保你總是釋放資源,某些設計技術,如果能夠始終如一地加以應用,將能對你有所幫助。在上文中我們討論了為重要數據結構設立語義不變項可以怎樣引導內存解除分配決策。考慮一下,“按合約設計”(109頁)可以怎樣幫助你提煉這個想法。 練習: 22. 有些C和C++開發者故意在解除了某個指針引用的內存的分配之后,把該指針設為NULL。這為什么是個好主意?  (解答在292頁) 23. 有些Java開發者故意在使用完某個對象之后,把該對象變量設為NULL,這為什么是個好主意?  (解答在292頁) 相關內容: l 原型與便箋,53頁 l 重構,184頁 l 易于測試的代碼,189頁 l 無處不在的自動化,230頁 l 無情的測試,237頁 挑戰: l 如果有人——比如銀行柜臺職員、汽車修理工或是店員——對你說蹩腳的借口,你會怎樣反應?結果你會怎樣想他們和他們的公司?
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看