- P7
把主要精力集中於構建活動,可以大大提高程式員的生產率。
在最近的一個項目中,對於這一點,我是深有體會。我們花了很長的時間做設計,結果接下來的許多工作都在愉快的心情下完成。我覺得
P28
的那個食物鏈的例子更有說服力,健康的生態環境中,海鷗吃新鮮的鮭魚,鮭魚吃新鮮的青魚,青魚吃新鮮的水蝽。這是一條健康的食物鏈。
如果環境被汙染了,水蝽在汙染的水域遊泳,那麼海鷗,食物鏈的最後一環吃下的不僅僅是是不健康的鮭魚體內的垃圾,還有青魚,水蝽體內的汙染物。軟體開發中,架構師吃掉需求,設計師吃掉架構,程式員,軟體食物鏈的最後一環,消化掉設計。如果一開始就被汙染了,我們就不要指望程式員快樂了。整個軟體都會具有放射性,周身都是缺陷,絕對導致程式員脾氣暴躁、營養失調。在我們規模不大的團隊裡,一個人身兼數職,傷害更大。所以,項目一開始就決定了它能否成功。
- P7
原始碼——往往是對軟體的唯一精確描述
其實我們不必為沒有精確的文檔沮喪,不是嗎?
- P13 常見的軟體隱喻
好的隱喻可以讓我們思考更多的問題,並走上正確的道路。我們是在
Writing Code,還是 Growing a System 還是 System Accretion
或是 Building Software ?
做不同軟體有不同的方法,不要拘泥。
- P24
避免使用錯誤的方法製造正確的產品
往往我們在軟體開發中會很強調測試。的確、測試是品質的保證。但是測試只保證有品質的代碼,卻不保證有品質的設計。
- P42 需求的 checklist
其實我們不必去照本宣科的寫需求分析書什麼的,做需求分析即使是在大腦中,口頭交流上完成,也是有這麼一個過程。落下文字固然是好的,但並不是重點。關鍵在於做不做。是
否詳細定義了系統的全部輸入,包括來源、精度、取值範圍、出現頻率。是否詳細定義了系統全部輸出,包括目的,精度,取值範圍、出現頻率,格式?是否定義了
機器記憶體和剩餘磁碟空間的最小值?是否詳細定義了系統的可維護性,包括適應特定功能的變更、作業環境的變更、與其他軟體的介面的變更能力?
書中列的遠比我這裡列出的多,非常值得一讀。定下這些是很重要的,我覺得合理的遊戲開發,是有一個相對穩定的策劃方案,和一些已經完成完成的美術資源。大
部分的變更都留在下一版本去做。策劃和美術永遠為下一個版本工作,而程式可以根據相對穩定的需求做設計。這樣做,即使第一個版本是不可玩的,扔掉,也是能
讓遊戲最終成功。
- P46 資料設計
我曾經很迷惑項目文檔到底要寫什嗎?這裡列舉的一些東西解開了我一些疑惑。如果你選擇使用一個順序訪問的列表表示一組資料,就應該說明為什麼順序訪問比隨機訪問更好。(往往隨機訪問更為高效)在構建期間,這些資訊讓你能洞察架構師的思想。在維護階段,這種洞察力是無價之寶。
後面 P58 有個更為有趣的例子:Beth 想做丈夫 Adbul
家祖傳的燉肉。Adbul
說,先撒上胡椒和鹽,然後去頭去尾,最後放在鍋裡蓋上蓋子燉就好了。Beth
就問了,“為什麼要去頭去尾呢?” Abdul
回答說,我不知道,我一直這麼做,這要問我媽。他打電話回家一問,母親也說不知道,她一直這麼做,這個問題要問奶奶。母親就打了個電話給奶奶,奶奶回答說,“我不知道你為什麼要去頭去尾,我這麼做是因為我的鍋太小了裝不下”:D
架構應該描述所有主要決策的動機。
- P48 國際化和本地化
國際化常常被稱為 i18n 是因為 Internationalization
這個單詞太長了,I 和n 之間有 18 個字母。
同理,通常本地化簡寫為 l10n
。這個工作一定要在構架期想好啊,到底我們需不需要
i18n 或者支援 l10n 就夠了,到底用 UTF-16 還是 UTF-8
還是 ascii
串就可以。是代碼中嵌入字串,還是封裝到一個類,還是可以放去一個設定檔。我們現在的項目,一開始考慮岔了,結果後來花了很多精力重構。好在發現問題不
算晚,否則代價更高。引申的想一想,發現問題我們應該最早的解決,不要怕做出錯誤的決定,因為更可怕的是,意識到錯誤後,因為害怕修正錯誤的代價過大,而
一拖再拖。代碼的 bugfix
大家都知道應該立刻做,但是設計失誤卻容易被放過,將就著做下去。
- P51 過度工程
這個問題把握好並不容易。一方面,我們希望系統健壯,如果組成系統的各個部分只在最低限度滿足健壯性要求,那麼整體通常是達不到要求的。軟體健壯性不取決於最薄弱的一環,而是等於所有薄弱環節的乘積。構架應該指出每個部分,程式員為了謹慎而寧可做過度工程,還是做出簡單的能工作的東西就夠了。有些東西是不應該過分花精力的,這個錯誤我們也犯過,尤其一些一開始就知道以後很可能要重構的部分,大量的精力花在裡面很浪費。
- P62 選擇程式設計語言
我曾經也覺得 C++ 是萬能的,這種想法很多 C++
程式員也有。但是無可否認,每種語言的表達力是不同的。書在這頁有一張表,如果
C 的表達能力是 1 的話,C++ 和 Java 就是 2.5 。而 perl
和 python 卻有 6
。這就是我們選擇遊戲邏輯指令碼編寫的原因之一。另外對語言的熟悉程度是很影響程式員的效率的,所以我們不能獨立的看語言本身的表達能力。P63
有個例子,用一群 Fortran 背景的程式員去用 C++
編寫一個新系統,結果他們編寫出的是偽裝成 C++
的 Fortran 代碼。他們扭曲 C++ 來類比 Fortran
的不良特性並且忽略了 C++
豐富的物件導向能力。我們這裡有個現成的例子,一個
C++ 程式員用 C++/C 的方式寫 Lua
,結果可想而知。到現在我還在叮囑他,一定要理解,再理解
Lua 。lua
不是 C 。
- P68 Programming into a Language
注意,這裡是 into 而不是 in 。書這裡用了一個 vb
的例子來說明,恰好我也有個例子。我們現在用 C++
構建系統,C++
裡有個相當麻煩的東西,就是單件的生存期問題。一個
singleton
到底什麼時候建立出來,什麼是否析構,相信很多
C++
程式員在構建大系統的時候都頭痛過。據我所知,我們公司別的項目的同事到現在還在頭痛這個問題。這次我做了一個約定,禁止任何模組的代碼構造靜態對象,也就是說,任何在
main 函數前自動的物件建構過程和 main
函數之後的自動析構過程都是不允許的。然後我們有一整套管理單件的方法供使用,這個問題被很好的解決了。我們再也沒有為某個單件什麼時候構造出來的,或是為什麼他提前析構了的問題煩惱過。
- P78 管理複雜度的重要性
我們做軟體,就是在和問題的複雜度做鬥爭。有三個問題需要注意:用複雜的方法解決簡單的問題;用簡單但錯誤的方法解決複雜的問題;用不恰當的複雜方法解決複雜的問題。
- P80 high fan-in 和 high fan-out
高內聚,低耦合很容易被重視。但是高扇入低扇出有時候會被忽略。這裡是說,我們應該盡量的大量的使用某個低層次上給定的類(high
fan-in)
而每個類都應該盡量少使用其他的類(控制在7個之下)。
- P83 子系統間盡量減少聯絡
書裡的例子很貼切。一個通用的規則是,如果 A
系統用到 B ,B 又用到 C ,那麼 C 就不要再用到 A
了。系統層的設計圖應該是無環圖。
- P91
封裝不僅僅是有簡化過的模型看到複雜的概念,而且同時還不能讓你看到複雜概念的任何細節
隱藏資訊的重要性毋庸質疑。所以我們現在不僅用
C++ 的 private
隱藏資訊。還用介面的方法,不在標頭檔暴露任何設計細節。另外,任何一個不滿足現狀的程式員,對自己以前的代碼一定不會滿意。但是複用老的不滿意的代碼並非壞事。我們需要做的是,重用的時候,把老的東西隱藏起來。
- P99 預料不同程度的變化
好的設計人員應該對以後可能變化的部分很敏感。這點很有體會,我自己就是這樣一步步過來的。做一個決定之前,想想如果他做錯了的後果。估計未來改變的成本,以作出合適的設計,非常的重要。系統為了適應每種變化而做過度設計又是不合適的。到底怎樣設計,需要良好的感覺。
- P101 耦合的種類
鬆散耦合是每個系統設計人員所追求的東西。但是其標準往往把握不準。舉個簡單的例子,不一定恰當。在我最早的設計裡,系統把座標這個東西封裝成一個叫做
point ,以後參數傳遞都傳 point * ,而不是 x,y
。這看使很合理。但是,這的確增加了耦合度。因為每個類都需要知道
point
的細節。很多情況下,用簡單類型做參數傳遞反而更合適。(到底傳
point * 還是 x,y 依舊要根據實際情況靠量)
參數過多也會導致耦合度的增加,從這個角度看,x,y
是兩個參數, point *
是一個參數。關於耦合程度的問題,沒有絕對唯一的標準。書裡的闡述和總結非常值得一看。
- P103 查閱常用的設計模式
設計模式這個東西,給程式員帶來的最大的好處就是增加了交流的便捷。一個人可以思考的深度取決於他用于思考的語言掌握的詞彙量。設計模式也可以給程式員帶
來這種便捷。其實常用的設計模式,即使你沒有看過《設計模式》這本書,只要你是一個經驗豐富的程式員,這些估計大多考慮過。但是,給設計模式起個名字,卻
可以加快夥伴間的交流。不過必須警惕一個陷阱,那就是為模式而模式。強迫代碼去使用某個模式是很危險的。
- P106 避免失誤
失敗的經驗比成功的經驗重要。如今網遊開發尤甚。我們的遊戲能成功,是因為我們在失敗後吸取教訓,小心謹慎。我們的代碼未必品質高很多,但是很多業內同仁做出的東西吸取了許多人家成功的經驗卻失敗了,正是因為他們缺少對失敗教訓的學習。
- P111
自上而下和自下而上的設計方法
很多人都贊同自上而下的設計方法,把問題逐步分解,再分而治之。而我喜歡至下而上的設計方法。先把絕對要做的模組做了,再考慮怎麼把他們搭起來。但是我在
實際操作的時候往往是,自下而上的做,自上而下的思考。書這裡對兩種方法的優缺點的總結非常精闢。我特別同意最後的結論,這兩種方法並不是互斥的——你會受益於二者的相互協作。
- P114
開發人員不把原型代碼當作可以拋棄的代碼。
這個問題很嚴重,我多次看到過了。做原型真的是一個好方法,但一定要明白,有些代碼寫出來就是為了以後扔掉的。
- P118 使用數位相機
設計文檔不一定要詳細的寫成規範的文檔,重要的是我們做了設計並保留下來,而不是寫出漂亮的文檔(去蒙投資?)數位相機似乎是一個好東西,這樣可以把草稿
紙上的亂塗亂畫保留下來。我一直覺得鉛筆是設計師最好的夥伴。開會的白板也是,雖然現在已經有電子化的白板,但是,昂貴的東西不是所有開發人員都需要的。
- P131 不要讓 ADT 依賴於儲存介質
這點早就意識的到,類裡面最好不要有 readfromfile
, writetofile
這樣的方法。但是為了方便,往往又會加上這些方法。最終的結果是,依賴檔案帶來的不便總是比便利要多。同理,依賴檔案名稱也是不恰當的。可悲的是,有些錯誤犯一次往往不夠。意識到這樣做的不好是很容易的,真正杜絕它是另一件事。
- P143 在萬不得已時通過 private
繼承來實現“has a”的關係
private
繼承的主要原因是讓外層的類能夠訪問內層被包含類的
protected 成員。根據我自己的經驗,讓類有 protected
成員本身就不是一件好事。我剛學 C++
的時候,很多類都有大量的 protected
,但現在,只剩下了 private 和 public 。雖然偶有
protected
,但是我相信,那些都是可以通過改良設計然後去掉的。我不喜歡教條,如果有教條說,不準用
protected ,我會很反感。而實現上,我的感覺會排斥
protected 的使用。
- P174 對於超過 200
行代碼的子程式來說,沒有哪項研究發現它更夠降低成本和/或降低出錯率
這是個誰都懂的道理,但是錯誤我自己也犯過。早幾年,我為大話西遊編寫了一個圖文混排的函數,可以在聊天或是其他螢幕文字中同時顯示不同的文字,有不同的
顏色,狀態,效果,還可以顯示動畫表徵圖。當時圖一時方便,還有一些效率原因,我寫了一個幾百行的函數。再接下來的幾年裡,我為這個函數多次頭痛過,出過不
只三次的 bug
。直到在夢幻西遊的項目裡,這些模組得到全部重寫才舒了一口氣。長達千行的子程式我也在一些地方見過,那也是非常可怕的。並非所有程式員可以真正認識到這
樣做的可怕程度吧。
- P176 使用 C++ 中的 const
關鍵字來定義輸入參數
能夠正確充分的使用 const 是合格的 c++
程式員評判標準之一。
- P176
不要把子程式的參數用做工作變數
不要因為想當然的效率因素使用輸入參數做工作變數,編譯器會幫你最佳化的。如果這樣做,至少在我們公司內部的
codeview 上是要被嚴重警告的。
- P210
確認留在代碼中的錯誤訊息是友好的
讀這段的時候想到同事給我講的一個故事:他以前的同事因為敲出髒話被開掉了,起因是他的老闆(也是一個程式員)在調試代碼的時候被某個模組的出錯資訊罵了一通
- P220
通過虛擬碼編程過程建立子程式
並非所有的代碼編寫前都要有虛擬碼的。更古老一點的編程方法裡,要求編寫程式前先畫框圖。這種方法我也學習過,但始終不覺得自然。虛擬碼如果也有條條框
框,說不定我也不會去用。但是,實現複雜演算法時,這種方法依舊被我使用的最多。一個附帶的好處就是,先有了注釋,然後刪掉多餘的;而不是先有代碼,再加上
注釋。
- P238 資料認知測試
哈哈,我得了 28 分(或者是 27.5)
,而且沒有被作者的陷阱框住。
- P240
隱式變數聲明對於任何一種語言來說都是最具危險性的特性之一
早年,我們用 Lua 的第 4
版寫指令碼,結果在這個問題上吃過幾次大苦頭。好在
lua 在第 5 版後增加了 metatable
可以有辦法避免這個問題。那就是給 _G 定義一個
__newindex 的 method 。例子在 lua 包裡的 test
目錄下可以找到,pil 裡也應該有提到。
- P244 Rob Pike 建議使用 0xDEADBEEF
這一常量來填充記憶體,因為在調試器裡很容易識別它。
其實還可以用 0xBADF00D 也很有趣,可惜是 7
個字母。有個朋友用這個做網名,我還蹭過一頓飯。:D
- P246
儘可能縮短變數的存活時間
這是一個淺顯但實用的道理。p248
裡還提到,全域變數的存活時間最長,就憑這一點,我們也應該避免使用。使用全域變數和不使用,是關易寫代碼和易讀代碼的區別;也是“方便性”和“智力可管
理性”的理念區別。這一節還有個被量化的概念,變數的跨度。把一個臨時變數重複使用,在增加了存活時間的同時也增加了其跨度,所以出現了很糟糕的味道。這
一點在 P255 又談了一次。
- P253 綁定時間
一般說來,變數的綁定時間越晚,靈活性越好。這裡的關於綁定時間的總結很不錯。分別為:
- 編碼時綁定 (使用 magic number)
- 編譯時間綁定 (使用命名常量)
- 載入時綁定 (讀註冊表,設定檔等)
- 對象執行個體化時綁定 (建立對象時讀入)
- 即時 (每次操作時讀入)
綁定越晚,在增加靈活性的同時,也增加了耦合度。
- P256 避免讓代碼具有隱含含義
這裡有個例子其實我曾經也類似的幹過,變數
pageCount
在非負的時候表示已列印紙張的數量,否則,-1
表示有錯誤發生。這在技術領域裡被稱為“混合耦合”是應該避免的。pageCount
客串了一個 boolean
類型。如果真的想節省一個變數的空間的話,我覺得使用
union 可能能好些。
- union {
- int pageCount;
- struct {
- unsigned int unused:31;
- unsigned int pageError:1;
- };
- };
-
或許這也不是個更好的方案。
- P298
在程式生命期中儘早決定國際化/本地化策略
關於這個問題,我們吃過的苦頭足夠說明問題。另外,MS
倡導的 _T()
宏不一定是一個優秀的方案。如果要支援 unicode
,UTF-8 是個不錯的選擇,雖然不總是最好的。UTF-8
表示漢字時 50% 超出的儲存空間好好考慮一下。
- P301 更安全的做法是使用 strlcpy() 或
strcpy_s()
很高興看到這句話是在譯註裡列出,可見譯者是用了心的。類似的譯註,我在侯傑的譯書中見過。不過
strlcpy 和 strncpy 的行為還是有區別的,strcpy
往往可以簡單的用 strncpy 取代,但是 strlcpy
卻沒有這個效果。不過我也傾向於用 strlcpy ,它同
strcpy 一樣的高效,而且的確也更安全。strncpy
在參數為 NULL 時會出錯。
- P305 把 enum
的第一個元素留做非法值
這是這本書讀到現在碰到的第一個以前沒想過的技巧。一直我都是把非法值定義成最後一個
enum
值,或者定義成一個很大的特殊數字。書這裡的道理很充分,因為一些沒有合理初始化的變數往往是
0 ,把 0 作為非法值更容易捕捉到錯誤。
- P309 Monty Python 為 20 世紀 60
年代英國經典電視連續劇,python
語言由此得名。
還是譯註,很有趣 我以前並不知道 python 的名字是怎麼來的。
- p329 簡化複雜的指標運算式
上學的時候,我經常寫
p->q->r->s.data
這種運算式,當我工作後,被這種運算式坑過幾次後,用額外的指標變數來提高代碼清晰度(p327)
同樣成了我的信條。另外 p329
介紹的畫一個圖的方法也是被經常使用。
- P330
分配一片保留的記憶體後備地區
用樣來保證程式崩潰的緊急處理所需要的記憶體是個簡潔的方法。
- P364 濫用case語言
把不同的控制結構混在一起用的確是非常糟糕的習慣。不過有時候的確可以帶來高一些的效率。我們可以不用它,但是不能讀不懂它。這讓我想起上次去跟同事一起出差,在火車上他談到的一個例子:
- int n = (count + 3) / 4;
- switch (count % 4) {
- case 0: do {
- dosomething0();
- case 1:
- dosomething1();
- case 2:
- dosomething2();
- case 3:
- dosomething3();
- }
- while (--n>0);
- }
P374 在 while 迴圈更適用的時候,不要使用
for 迴圈
- P376 一個迴圈只做一件事情
我在自己的書中談效能時也提到過這一點,其實,合并幾件事情在一個迴圈做不一定可以得到更高的效能。
- P377
避免出現依賴迴圈下標最終取值的代碼
還是一個早就明白的道理,但是錯誤一犯再犯。
- P384 for 迴圈變數的範圍
VC 中的語義跟 C++ 標準定義的不一樣。MS
推薦一種解決方案:
-
#define for if (0); else for
- P397
如果為我工作的程式員用遞迴去計算階乘,那麼我寧願換人
說的好。如果我帶的新同事在 C
語言裡用遞迴計算費伯納西數列,估計我也會教育一下的。
- P399 讓代碼不包含 goto
並不是目的,而只是結果,把目的地組中在消除 goto
上面是於事無益的。
p408
還有一句,如果程式員知道存在替換方案,並且也願意為使用
goto 辯解,那麼用 goto 也無妨。
- P408
軟體開發這一領域是在限制程式員對代碼的使用中得到發展的
這裡舉的例子,允許根據行數或者標號調用子程式,是我早年用
basic 時夢寐以求的。不過那個時候 basic
過於簡陋,期待這種功能是迫不得已。實在不行的時候還要自己擴充
basic 解譯器。
- P429
最後是去找一個好的方案而且同時避免引發災難,而不要試圖去尋找最佳的方案。
我想補充,如果你知道一種更好的方案但是不能完全避免災難的時候,請注釋這種更好的方案。
- P442
這項建議(在等於運算式中的常數寫在前面以避免把
== 錯誤的敲成 =
的問題)與按造數軸排列的建議相衝突。我個人偏向於使用數軸排序法,讓編譯器來告訴我有沒有無意寫出的指派陳述式。
我也不希望把常數寫在前面,但老說不清楚原因。
- 關於剩下的 400
頁:代碼大全》的確是本好書,雖然厚達九百頁,但是卻沒有什麼廢話。讀書筆記只做到這
裡,雲風也意尤未盡的看完了書的最後一頁。缺少後一半的讀書筆記並不是說書的後半本不及前半本精彩。相反,我認為後面的某些章節對我的啟發更大。只是,後
半本書是我花了半個月時間,每天臨睡前躺在床上看完的。早上起來,已經懶的在機器上敲下什麼文字了。喜歡這篇讀書筆記的同學,推薦去買一本《代碼大全》耐
心的閱讀全部。