程序員修煉之道前端
1 個人源碼讓貓給吃了... 3java
2 軟件的熵... 5node
3 石頭湯與煮青蛙... 7ios
4 足夠好的軟件... 9程序員
5 你的知識資產... 11正則表達式
6 交流!... 16算法
7 重複的危害... 21shell
8 正交性... 29數據庫
9 可撤消性... 37編程
10 曳光彈... 40
11 原型與便箋... 44
12 領域語言... 48
13 估算... 54
14 純文本的威力... 59
15 shell遊戲... 64
16 強力編輯... 68
17 源碼控制... 71
18 調試... 74
19 文本操縱... 81
20 代碼生成器... 84
21 按合約設計(1) 87
21 按合約設計(2) 92
22 死程序不說謊... 99
23 斷言式編程... 101
24 什麼時候使用異常... 104
25怎樣配平資源... 108
1 個人源碼讓貓給吃了
注重實效的程序員的特徵是什麼?咱們以爲是他們處理問題、尋求解決方案時的態度、風格、哲學。他們可以越出直接的問題去思考,老是設法把問題放在更大的語境中,老是設法注意更大的圖景。畢竟,沒有這樣的更大的語境,你又怎能注重實效?你又怎能作出明智的妥協和有見識的決策?
他們成功的另外一關鍵是他們對他們所作的每件事情負責,關於這一點,咱們將在「個人源碼讓貓給吃了」中加以討論。由於負責,注重實效的程序員不會坐視他們的項目土崩瓦解。在「軟件的熵」中,咱們將告訴你怎樣使你的項目保持整潔。
大多數人發現本身很難接受變化,有時是出於好的理由,有時只是由於固有的惰性。在「石頭湯與煮青蛙」中,咱們將考察一種促成變化的策略,並(出於對平衡的興趣)講述一個忽視漸變危險的兩棲動物的警世傳說。
理解你的工做的語境的好處之一是,瞭解你的軟件必須有多好變得更容易了。有時接近完美是唯一的選擇,但經常會涉及各類權衡。咱們將在「足夠好的軟件」中探究這一問題。
固然,你須要擁有普遍的知識和經驗基礎才能贏得這一切。學習是一個持續不斷的過程。在「你的知識資產」中,咱們將討論一些策略,讓你「開足馬力」。
最後,咱們沒有人生活在真空中。咱們都要花大量時間與他人打交道。在「交流!」中列出了能讓咱們更好地作到這一點的幾種途徑。
注重實效的編程源於注重實效的思考的哲學。本章將爲這種哲學設立基礎。
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)指令的編譯器尚未發明出來,在「領域語言」中咱們給出了一些適度的建議,你能夠自行加以實施。
最後,咱們都是在一個時間和資源有限的世界上工做。若是你善於估計出事情須要多長時間完成,你就能更好地在二者都很匱乏的狀況下生存下去(並讓你的老闆更高興)。咱們將在「估算」中涵蓋這一主題。
在開發過程當中牢記這些基本原則,你就將能編寫更快、更好、更強健的代碼。你甚至可讓這看起來很容易。
7 重複的危害
給予計算機兩項自相矛盾的知識,是James T. Kirk艦長(出自Star Trek,「星際迷航」——譯註)喜歡用來使四處劫掠的人工智能生命失效的方法。遺憾的是,一樣的原則也能有效地使你的代碼失效。
做爲程序員,咱們收集、組織、維護和利用知識。咱們在規範中記載知識、在運行的代碼中使其活躍起來並將其用於提供測試過程當中所需的檢查。
遺憾的是,知識並不穩定。它變化——經常很快。你對需求的理解可能會隨着與客戶的會談而發生變化。政府改變規章制度,有些商業邏輯過期了。測試也許代表所選擇的算法沒法工做。全部這些不穩定都意味着咱們要把很大一部分時間花在維護上,從新組織和表達咱們的系統中的知識。
大多數人都覺得維護是在應用發佈時開始的,維護就意味着修正bug和加強特性。咱們認爲這些人錯了。程序員須持續不斷地維護。咱們的理解逐日變化。當咱們設計或編碼時,出現了新的需求。環境或許變了。無論緣由是什麼,維護都不是時有時無的活動,而是整個開發過程當中的例行事務。
當咱們進行維護時,咱們必須找到並改變事物的表示——那些嵌在應用中的知識膠囊。問題是,在咱們開發的規範、過程和程序中很容易重複表述知識,而當咱們這樣作時,咱們是在向維護的噩夢發出邀請——在應用發佈以前就會開始的噩夢。
咱們以爲,可靠地開發軟件、並讓咱們的開發更易於理解和維護的唯一途徑,是遵循咱們稱之爲DRY的原則:
系統中的每一項知識都必須具備單1、無歧義、權威的表示。
咱們爲什麼稱其爲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 start.distanceTo(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 = start.distanceTo(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)的組件:獨立,具備單1、良好定義的目的(Yourdon和Constantine稱之爲內聚(cohesion)[YC86])。若是組件是相互隔離的,你就知道你可以改變其中之一,而不用擔憂其他組件。只要你不改變組件的外部接口,你就能夠放心:你不會形成波及整個系統的問題。
若是你編寫正交的系統,你獲得兩個主要好處:提升生產率與下降風險。
提升生產率
l 改動得以局部化,因此開發時間和測試時間得以下降。與編寫單個的大塊代碼相比,編寫多個相對較小的、自足的組件更爲容易。你能夠設計、編寫簡單的組件,對其進行單元測試,而後把它們忘掉——當你增長新代碼時,無須不斷改動已有的代碼。
l 正交的途徑還可以促進複用。若是組件具備明確而具體的、良好定義的責任,就能夠用其最初的實現者不曾想象過的方式,把它們與新組件組合在一塊兒。
l 若是你對正交的組件進行組合,生產率會有至關微妙的提升。假定某個組件作M件事情,而另外一個組件作N件事情。若是它們是正交的,而你把它們組合在一塊兒,結果就能作M x N件事情。可是,若是這兩個組件是非正交的,它們就會重疊,結果能作的事情就更少。經過組合正交的組件,你的每一份努力都能獲得更多的功能。
下降風險
正交的途徑能下降任何開發中固有的風險。
l 有問題的代碼區域被隔離開來。若是某個模塊有毛病,它不大可能把病症擴散到系統的其他部分。要把它切掉,換成健康的新模塊也更容易。
l 所得系統更健壯。對特定區域作出小的改動與修正,你所致使的任何問題都將侷限在該區域中。
l 正交系統極可能能獲得更好的測試,由於設計測試、並針對其組件運行測試更容易。
l 你不會與特定的供應商、產品、或是平臺緊綁在一塊兒,由於與這些第三方組件的接口將被隔離在所有開發的較小部分中。
讓咱們看一看在工做中應用正交原則的幾種方式。
項目團隊
你是否注意到,有些項目團隊頗有效率,每一個人都知道要作什麼,並全力作出貢獻,而另外一些團隊的成員卻總是在爭吵,並且好像沒法避免互相妨礙?
這經常是一個正交性問題。若是團隊的組織有許多重疊,各個成員就會對責任感到困惑。每一次改動都須要整個團隊開一次會,由於他們中的任何一我的均可能受到影響。
怎樣把團隊劃分爲責任獲得了良好定義的小組,並使重疊降至最低呢?沒有簡單的答案。這部分地取決於項目自己,以及你對可能變更的區域的分析。這還取決於你能夠獲得的人員。咱們的偏好是從使基礎設施與應用分離開始。每一個主要的基礎設施組件(數據庫、通訊接口、中間件層,等等)有本身的子團隊。若是應用功能的劃分顯而易見,那就照此劃分。而後咱們考察咱們現有的(或計劃有的)人員,並對分組進行相應的調整。
你能夠對項目團隊的正交性進行非正式的衡量。只要看一看,在討論每一個所需改動時須要涉及多少人。人數越多,團隊的正交性就越差。顯然,正交的團隊效率也更高(儘管如此,咱們也鼓勵子團隊不斷地相互交流)。
設計
大多數開發者都熟知須要設計正交的系統,儘管他們可能會使用像模塊化、基於組件、或是分層這樣的術語描述該過程。系統應該由一組相互協做的模塊組成,每一個模塊都實現不依賴於其餘模塊的功能。有時,這些組件被組織爲多個層次,每層提供一級抽象。這種分層的途徑是設計正交系統的強大方式。由於每層都只使用在其下面的層次提供的抽象,在改動底層實現、而又不影響其餘代碼方面,你擁有極大的靈活性。分層也下降了模塊間依賴關係失控的風險。你將經常看到像下一頁的圖2.1這樣的圖表示的層次關係。
對於正交設計,有一種簡單的測試方法。一旦設計好組件,問問你本身:若是我顯著地改變某個特定功能背後的需求,有多少模塊會受影響?在正交系統中,答案應
圖2.1 典型的層次圖
該是「一個」。移動GUI面板上的按鈕,不該該要求改動數據庫schema。增長語境敏感的幫助,也不該該改動記帳子系統。
讓咱們考慮一個用於監視和控制供暖設備的複雜系統。原來的需求要求提供圖形用戶界面,但後來需求被改成要增長語音應答系統,用按鍵電話控制設備。在正交地設計的系統中,你只須要改變那些與用戶界面有關聯的模塊,讓它們對此加以處理:控制設備的底層邏輯保持不變。事實上,若是你仔細設計你的系統結構,你應該可以用同一個底層代碼庫支持這兩種界面。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 {
Log.write("-> Entering " + thisJoinPoint.methodName);
}
}
}
若是你把這個方面編織(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的數學測驗要讓人以爲舒服得多。管理人員每每與工程師趣味相投:單1、容易的答案正好能夠放在電子表格和項目計劃中。
現實世界可以合做就行了!遺憾的是,今天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.一、9五、9八、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]。採用這種方法,你能夠快速地把現有組件裝配進新的配置,從而瞭解它們的工做狀況。
製做架構原型
許多原型被構造出來,是要爲在考慮之下的整個系統建模。與曳光彈不一樣,在原型系統中,單個模塊不須要能行使特定的功能。事實上,要製做架構原型,你甚至不必定須要進行編碼——你能夠用便箋或索引卡片、在白板上製做原型。你尋求的是瞭解系統怎樣結合成爲一個總體,並推遲考慮細節。下面是一些你能夠在架構原型中尋求解答的具體問題:
l 主要組件的責任是否獲得了良好定義?是否適當?
l 主要組件間的協做是否獲得了良好定義?
l 耦合是否得以最小化?
l 你可否肯定重複的潛在來源?
l 接口定義和各項約束是否可接受?
l 每一個模塊在執行過程當中是否能訪問到其所需的數據?是否能在須要時進行訪問?
根據咱們製做原型的經驗,最後一項每每會產生最讓人驚訝和最有價值的結果。
怎樣「不」使用原型
在你着手製做任何基於代碼的原型以前,先肯定每一個人都理解你正在編寫用過就扔的代碼。對於不知道那只是原型的人,原型可能會具備欺騙性的吸引力。你必須很是清楚地說明,這些代碼是用過就扔的,它們不完整,也不可能完整。
別人很容易被演示原型外表的完整性誤導,而若是你沒有設定正確的指望值,項目出資人或管理部門可能會堅持要部署原型(或其後裔)。提醒他們,你能夠用輕木和膠帶製造一輛了不得的新車原型,但你卻不會在高峯時間的車流中駕駛它。
若是你以爲在你所在的環境或文化中,原型代碼的目的頗有可能被誤解,你也許最好仍是採用曳光彈方法。你最後將獲得一個堅實的框架,爲未來的開發奠基基礎。
適當地使用原型,能夠幫助你在開發週期的早期肯定和改正潛在的問題點——在此時改正錯誤既便宜、又容易——從而爲你節省大量時間、金錢,並大大減輕你遭受的痛苦和折磨。
相關內容:
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 (record.balance < 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。使用純文本,你能夠將其寫爲:
myprop.uses_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 archive.zip *.h *.c 或
tar cvf archive.tar *.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 'java.awt'
GUI
把前面的例子列出的各個文件裝入編輯器,搜索字符串「Java.awt」。把含有該字符串的文件的名字寫下來。
顯然,這樣的例子還能夠一直舉下去。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
2c6e776f2058580a 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)。類確保從調用者的視角來看,該條件老是爲真。在例程的內部處理過程當中,不變項不必定會保持,但在例程退出、控制返回到調用者時,不變項必須爲真(注意,類不能給出無限制的對參與不變項的任何數據成員的寫訪問)。
讓咱們來看一個例程的合約,它把數據值插入唯1、有序的列表中。在iContract(用於Java的預處理器,可從[URL 17]獲取)中,你能夠這樣指定:
/**
* @invariant forall Node n in elements() |
* n.prev() != null
* implies
* n.value().compare To(n.prev().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基類java.awt.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*Current.abs;
-- 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 arr.length > 0
int i = 1;
// Loop invariant: m = max(arr[0:i-1])
while (i < arr.length) {
m = Math.max(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
result.next.previous = result -- Check the newly
result.previous.next = 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語句的選擇開關再也不是預期的一、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 (iter.hasmoreElements () {
Test.ASSERT(iter.nextElements() != null);
object obj = iter.nextElement();
// ....
}
ASSERT中的.nextElement()調用有反作用:它會讓迭代器越過正在讀取的元素,這樣循環就會只處理集合中的一半元素。這樣編寫代碼會更好:
while (iter.hasmoreElements()) {
object obj = iter.nextElement();
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 (socket.read(name) != OK) {
retcode = BAD_READ;
}
else {
processName(name);
if (socket.read(address) != OK) {
retcode = BAD_READ;
}
else {
processAddress(address);
if (socket.read(telNo) != OK) {
retcode = BAD_READ;
}
else {
// etc, etc...
}
}
}
return retcode;
幸運的是,若是編程語言支持異常,你能夠經過更爲簡潔的方式重寫這段代碼:
retcode = OK;
try {
socket.read(name);
process(name);
socket.read(address);
processAddress(address);
socket.read(telNo);
// etc, etc...
}
catch (IOException e) {
retcode = BAD_READ;
Logger.log("Error reading individual: " + e.getMessage());
}
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 (!f.exists()) {
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);
cRec.balance = 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) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
}
在測試時一切彷佛都很好。可是,當代碼投入實際工做,若干小時後它就崩潰了,抱怨說打開的文件太多。由於writeCustomer在有些情形下不會被調用,文件也就不會被關閉。
這個問題的一個很是糟糕的解決方案是在updateCustomer中對該特殊狀況進行處理:
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer(fName, &cRec);
if (newBalance >= 0.0) {
cRec.balance = 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) { // /
cRec.balance = 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 {
tmpFile.delete();
}
}
該例程使用了一個臨時文件,無論例程怎樣退出,咱們都要刪除該文件。finally塊使得咱們可以簡潔地表達這一意圖。
當你沒法配平資源時
有時基本的資源分配模式並不合適。這一般會出如今使用動態數據結構的程序中。一個例程將分配一塊內存區,並把它連接進某個更大的數據結構中,這塊內存可能會在那裏呆上一段時間。
這裏的訣竅是爲內存分配設立一個語義不變項。你需要決定誰爲某個彙集數據結構(aggregate data structure)中的數據負責。當你解除頂層結構的分配時會發生什麼?你有三個主要選擇:
1. 頂層結構還負責釋放它包含的任何子結構。這些結構隨即遞歸地刪除它們包含的數據,等等。
2. 只是解除頂層結構的分配。它指向的(沒有在別處引用的)任何結構都會被遺棄。
3. 若是頂層結構含有任何子結構,它就拒絕解除自身的分配。
這裏的選擇取決於每一個數據結構自身的情形。可是,對於每一個結構,你都須明確作出選擇,並始終如一地實現你的選擇。在像C這樣的過程語言中實現其中的任何選擇均可能會成問題:數據結構自身不是主動的。在這樣的情形下,咱們的偏好是爲每一個重要結構編寫一個模塊,爲該結構提供分配和解除分配設施(這個模塊也能夠提供像調試打印、序列化、解序列化和遍歷掛鉤這樣的設施)。
最後,若是追蹤資源很棘手,你能夠經過在動態分配的對象上實現一種引用計數方案,編寫本身有限的自動垃圾回收機制。More Effective C++[Mey96]一書專設了一節討論這一話題。
檢查配平
由於注重實效的程序員誰也不信任,包括咱們本身,因此咱們以爲,構建代碼、對資源確實獲得了適當釋放進行實際檢查,這老是一個好主意。對於大多數應用,這一般意味着爲每種資源類型編寫包裝,並使用這些包裝追蹤全部的分配和解除分配。在你的代碼中的特定地方,程序邏輯將要求資源處在特定的狀態中:使用包裝對此進行檢查。
例如,一個長期運行的、對請求進行服務的程序,極可能會在其主處理循環的頂部的某個地方等待下一個請求到達。這是肯定自從上次循環執行以來,資源使用不曾增加的好地方。
在一個更低、但用處並不是更少的層面上,你能夠投資購買能檢查運行中的程序的內存泄漏狀況(及其餘狀況)的工具。Purify(www.rational.com)和Insure++(www.parasoft.com)是兩種流行的選擇。
相關內容:
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 若是有人——好比銀行櫃檯職員、汽車修理工或是店員——對你說蹩腳的藉口,你會怎樣反應?結果你會怎樣想他們和他們的公司?