The Joel on Software Translation Project:讓錯的程式看得出錯

From The Joel on Software Translation Project

Jump to: navigation, search

讓錯的程式看得出錯

作者:周思博 (Joel Spolsky)
譯:Paul May 梅普華
Wednesday,May 11,2005
A part of Joel on Software,http://www.joelonsoftware.com

時間回到1983年九月,我第一個真正的工作是在以色列的Oranim。這家大型麵包工廠每晚都用六個貨機般大的巨型爐子烤出為數十萬的麵包。

我第一次走進那家麵包廠時覺得裡頭實在髒得離譜。爐壁發黃機器生鏽而且到處都是油。

「這裡一直都這麼髒嗎?」我問道。

「什麼?你講這什麼話?」經理回答說。「我們才剛打掃過。這已經是幾週以來最乾淨的時候了。」

說得真好!

我花了好幾個月每天早上打掃才真正瞭解他們的意思。對麵包工廠來說,乾淨是指機器裡沒有生麵糰在烤,垃圾堆裡沒有發酵的麵糰,而且地板上也沒有堆生麵糰。

乾淨並不是指爐子漆得雪白亮麗。爐子大概十年才會漆一次,並不會每天都來一回。乾淨也不是說把油擦得乾乾淨淨。事實上很多機器都得定期上油,一層薄淨的油通常暗示機器剛做過清潔保養。

DoughRounder.PNG

麵包工廠裡這整套乾淨的概念都得經由學習而來。圈外人不可能走進去就能說出哪裡乾淨哪裡髒。圈外人絕不會想到要看麵糰滾圓機(把方麵糰滾成球形的機器,見右邊附圖)內壁有沒有刮乾淨。圈外人會覺舊爐子外壁鑲板掉色是有問題的,因為鑲板很很顯眼。不過麵包師傅根本不在意爐子的塗漆開始發黃。因為麵包的味道還是一樣棒。

在麵包工廠待兩個月,你學會如何「看出」乾淨。

程式碼也是一樣的。

當你剛開始寫程式或嘗試讀用新語言寫的程式時,所有程式碼看起來都一樣神秘不可解。 而在瞭解該種程式語言前,你連明顯的語法錯誤都看不出來。

在學習的第一階段,你會開始發現一種我們通常稱為「編程風格」的東西。於是你開始注意那些不遵循縮排標準的程式碼和使用多個大寫字母的變數。

也就是這個階段你會說:「該死的混蛋,我們這裡一定要定出一些一致的編程風格!」 然後第二天寫出一份你們團隊用的編程風格,接下來用六天來討論One True Brace Style(譯註:就是K&R style),然後再花三星期把舊程式碼改寫成符合One True Brace Style,一直做到經理發現並責怪你把時間浪費在不能賺錢的事為止。你想想其實不需要一次全部改好,看到哪裡改到哪裡也沒什麼關係。於是有一半的程式碼已經改成True Brace Style,而沒多久你就忘記這件事了。 接下來你就開始滿腦子想著其他與賺錢無關的事,比如把某個字串類別換成另一個字串類別等等。

當你對某特定環境下的程式愈來愈精通時,就會開始學著看到其他東西。那些東西可能完全合法並符合編程風格,卻又會讓你擔心不已。

舉例來說在C語言裡:

char* dest,src;

這是合語法的程式碼;這可能符合你的編程規範,甚至可能是故意這樣寫的,不過如果你寫C的經驗夠,就會注意這種寫法把dest宣告成字元 指標卻把src宣告成字元而已,這可能是你的意思,不過也可能不是。反正這段程式看起來有點不對勁。

來看更細微的例子:

if (i != 0)
    foo(i);

這段程式是百分之百正確的;它符合大多數的編程規範也完全沒有錯誤,不過你可能會質疑if敘述所接的單敘述主體並未用大括號包起來,因為你腦子裡想到有人可能會插入另一行程式碼

if (i != 0)
    bar(i);
    foo(i);

...又忘記加上大括號,結果讓foo(i)變成永遠會執行!所以當你看到沒有用大括弧包起來的程式碼區段時,可能就會感覺到一絲絲讓你不舒服的氣味。

好啦,到目前為止我已經提到三種程式師的成就層級:

1。你不知道乾淨和髒有什麼分別。

2。你對乾淨有粗淺的認知,主要以是否符合編程規範為準。

3。你開始能嗅出藏在表面下不對勁的蛛絲馬跡。你會察覺這是問題並且找出來修正。

不過其實還有更高的層次,而這也就是我真正要說的:

4。你有計劃地架構程式碼,藉助能察覺問題的靈眼讓程式碼更正確。

這是真正的藝術:仔細地設計讓錯誤顯而易見的編程規範,藉此製作出穩固的程式。

所以現在我要帶你看一個小例子然後再展示一個通用的規則。你可以利用這個通則設計出創造增加程式穩固的編程規範。最後我會把主題導引到為某種匈牙利命名法(可能不是讓人們暈到的那種)進行辯護,並且批判某些環境(也可能不是你最常用的那種環境)下的例外處理。

不過如果你深信匈牙利命名法不是好東西,認為例外處理是從自巧克力奶昔以來最棒的發明,而且完全不想聽聽其他意見,沒問題,你可以改去羅力那裡看看好看的漫畫;反正你在這裡也沒什麼好看的;事實上在一分鐘內我就會拿出實際的程式碼範例,這些範例很可能會讓你在不爽前就暈睡過去了。沒錯。我想我的計畫是把你哄到沈沈入睡,趁你睡著無法抵抗時把「匈牙利命名法=好,例外處理=壞」的想法偷偷塞進你腦子裡面。

一個例子

Umbria.JPG

好了。提到這個例子。讓我們假裝你正在寫某種web應用程式,因為這陣子小朋友似乎都流行寫這玩意。

現在有一種叫跨站腳本漏洞(Cross Site Scripting Vulnearability)的安全漏洞,縮寫為XSS。我在這裡不談細節:你只需要知道在寫web應用程式時,一定要小心絕不能把使用者填入表單的任何字串直接傳回來。

舉例來說,如果你有一個網頁會讓使用者在編輯框輸入姓名,傳送後就會跳到另一個寫著「你好啊,張三!」(假設使用者的名字是張三)的網頁。很好,這就是個安全漏洞,因為使用者可能不輸入「張三」而輸入某種奇怪的HTML及JavaScript,這些奇怪的JavaScript就可能會做些低級事情,比如讀出你寫的cookie內容轉送到壞人的壞網站去。而這些低級事現在看起來就是你搞的鬼。

讓我們把程式用虛擬碼的方法寫出來。想像以下的程式

s = Request("name")

會由HTML表格讀取使用者輸入(一個POST的參數)。如果你曾經寫出下面的程式碼:

Write "你好," & Request("name")

那你的網站已經有讓XSS攻擊的漏洞了。光這樣就夠了。

你必須在複製回HTML之前先編碼才能避免這個漏洞。所謂編碼就是把"換成", 把>換成>,如此類推。所以

Write "你好," & Encode(Request("name"))

是絕對安全的。

所有來自使用者的字串都是不安全的。任何不安全的字串都得先編碼後才能輸出。

讓我們嘗試設計一組編程規範,確保當你犯這種錯時程式碼看起來就是錯的。如果程式碼有錯(至少看起來錯),就很有機會被修改或審視這段程式的人抓到。

可能方案一

方案一是將所有字串立即編碼,由使用者取得後馬上就進行:

s = Encode(Request("name"))

所以我們的規範會寫著:如果你看到沒有被Encode包住的Request,程式一定是錯的。

你開始訓練自己的眼睛找尋落單的Request,因為它們違反規範。

這是有用的,因為只要你遵循規範就不會有XSS問題。不過這並不是最好的架構。比方說你可能想要把這些使用者字串存到資料庫裡,這時候儲存以HTML編碼過的字串並不合理,因為字串有可能會用在HTML網頁以外的場合。假如是信用卡處理程式要用時編碼過的資料就會產生問題。大部份web應用程式開發都會依循一個原則:所有字串在內部都是編碼的,要等到送至HTML網頁的前一瞬間才會處理,因此這可能並不是正確的架構。

我們真的要能讓字串維持在不安全格式一段時間。

好吧,我再試看看。

可能方案二

如果建立一種編程規範,要求在寫出任何字串時必須加以編碼,是否可以滿足要求嗎?

s = Request("name")

// 很後面:
Write Encode(s)

現在當你看到一個落單沒有Encode跟著的Write時就知道有有問題了。

唉,這也不太好……有時候你的程式裡會有一小段的HTML碼,這種情況下是不能夠編碼的:

If mode = "linebreak" Then prefix = "<br>" // 很後面:
Write prefix

這照我們的規範來看是錯的,我們必須要在輸出時加以編碼:

Write Encode(prefix)

不過現在應該要新增一行的"<br>"卻被編碼成&lt;br&gt;,結果變成使用者可以看到的字元< b r >。這樣的解法也不對。

所以說有時候你不能在讀入字串時編碼,有時候你也不能在輸出時編碼,這兩種提案都不能用。可是沒有適當的編碼規範,我們還是有出下列問題的風險:

s = Request("name") ……好幾頁之後……
name = s ……好幾頁之後……
recordset("name") = name // 把名字存在資料庫中的姓名欄 ……好幾天後……
theName = recordset("name") ……好幾頁甚至好幾個月之後……
Write theName

我們還會記得要對字串編碼嗎?你在任何單一的地方都看不到問題。連可以嗅的地方都沒有。如果這種程式有一大缸子,要一大票偵探才能追蹤出所有字串的來源並確認是否已編碼。

正解

所以讓我提議一種能用的編程規範。我們只有一個規則:

所有來自使用者的字串都必須存在以"us"(表示Unsafe String,不安全字串)為字首的變數(或資料庫欄位)中。所有經HTML編碼或來自確認安全來源的字串都必須存在以"s"(表示Safe String,安全字串)為字首的變數中。

讓我們重寫程式,只是依規範重新命名變數,其他完全不動。

us = Request("name") ……好幾頁之後……
usName = us
……好幾頁之後……
recordset("usName") = usName
……好幾天後……
sName = Encode(recordset("usName"))
……好幾頁甚至好幾個月之後……
Write sName

新規範中值得注意的是,只要遵循編碼規範,不安全字串相關的錯誤一定可以由單一行的程式碼看出來

s = Request("name")

是之前的錯誤,因為你可以看到Request的結果被指派給以s開頭的變數,這違反了規則。Request的結果一定是不安全的,所以必須指派給以"us"開頭的變數。

us = Request("name")

一定沒問題。

usName = us

一定沒問題。

sName = us

一定是錯的。

sName = Encode(us)

一定是對的。

Write usName

一定是錯的。

Write sName

沒問題,下面也一樣沒問題

Write Encode(usName)

每一行程式光是看程式碼本身就足以檢查,而且如果每一行程式都對,組合起來整個程式也是對的。

終於好了,利用這套編碼規範,你的眼睛學著看到Write usXXX就知道是錯的,而且你也立即知道要如何修正。我知道一開始要看到錯誤的程式是有一點難,不過進行三個星期後你的眼睛就會習慣,就像麵包廠的工人看到大麵包工廠就會馬上說:「搞什麼鬼,這裡都沒人在掃哦!這算啥麵包廠。」

事實上我們可以再把規則延伸一點,把RequestEncode函數改名(或封裝)成UsRequestSEncode……換句話說,傳回不安全字串以及安全字串的函數要和變數一樣,分別要用UsS作為字首。現在看看程式碼:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SEncode(recordset("usName"))
Write sName

看到我們的成果沒?現在你可以看看等號兩邊的字首是否相同就能找到錯誤。

us = UsRequest("name") // 沒問題,兩邊都以US開頭
s = UsRequest("name") // 錯
usName = us // 對
sName = us // 一定錯。
sName = SEncode(us) // 一定對。

我還能再進一步把Write改名成WriteS並把SEncode改名成SFromUs

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SFromUs(recordset("usName"))
WriteS sName

這使得錯誤更加顯而易見。你的眼睛會學習「看出」可疑的程式碼,另外這也能協助你經由一般撰寫或閱讀程式碼的動作找到隱藏的安全漏洞。

讓錯的程式看得出錯是很棒沒錯,不過卻不是所有安全問題的最佳解答。它無法找到所有可能的問題或錯誤,因為你可能沒法子看過每一行程式碼。不過絕對比什麼都不做要好,而我很希望有套編碼規範能讓錯誤的程式碼至少看起來是錯的。你馬上就能獲得好處,每當程式師的眼睛掃過一行程式,就能檢查並防止某些特定的錯誤。

一個通則

這種讓錯誤程式看起來錯的作法有個前提,就是要讓對的東西在螢幕上緊靠在一起。當我看到某個字串時並要決定 程式碼正確與否,我必須知道字串出現的所有位置以及字串是安全的還是不安全的。我不希望這些資料出現在另一個檔案或是要捲動畫面才能看到的另一頁。我必須能當場看到,而這說的就是一套變數命名規範。

有很多其他的例子可以說明,只要把某些東西搬在一起就可以改善程式碼。大多數的編程規範都有如下的規則:

  1. 保持函數名稱簡短。
  2. 變數宣告的地方離使用的位置愈近愈好。
  3. 不要用巨集建立你個人專屬的程式語言。
  4. 不要使用goto
  5. 不要讓右括弧離左括弧超過一個畫面。

這些規則有一個共同點,就是儘量讓一行程式碼實際作用的相關資訊在畫面上愈近愈好。這樣能提高眼球找出程式實質運作內容的機會。

大體上我得承認我有點害怕會藏東西的程式語言功能。當你看到程式碼

i = j * 5;

……就C來說你至少會知道j會乘以5而結果會存到i

不過如果你在C++裡看到相同的片段,你什麼都不知道。在C++中唯一能知道真正發生什麼事的方法就是找出ij所屬的型別,而這個型別可能會在完全不一樣的地方宣告。因為j運算子*可能有過荷,在你要做乘法時會做些很機靈的事。而i運算子=可能也是過荷的,而兩者型別可能是不相容的,於是又呼叫到某個自動型別強制轉換的函數。光是檢查變數的型別還不足以確認,還得檢查實作該型別的程式碼才行,萬一實作時又有繼承其他型別就更麻煩了,因為你得回溯類別繼承的祖宗八代才能找到真正的程式碼,不巧又有用到別處的多型就真的有大麻煩了,因為光是知道ij宣告的型別並不夠,還得知道它們此刻的型別,這不知道要看多少的程式碼,而且依照計算理論的停機問題,你永遠都不能真的百分之百確定自己已經看完所有地方了(啊啊啊啊啊!!!)。

當你看到C++的i=j*5時你只能自求多福了,兄弟。這對我來說就降低了光看程式碼找出在問題的能力。

當然囉,理論上這應該沒什麼關係。當你做些重載運算子*之類聰明事時,只要為了要提供一個優美而安全的抽象罷了。天啊,其實j是個萬國碼字串型別,一個萬國碼字串乘以一個整數顯然是把正體中文轉成簡體中文的良好抽象作法,對嗎?

問題當然出在沒有絕對安全的抽象方法。我已經在抽象出錯定律裡討論很多了,所以不會在這裡重複。

Scott Meyers示範了各種抽象出錯(至少是C++)的型式以及所造成的傷害,他靠這個主題就創出一番事業了。(順便一提,Scott的書Effective C++第三版剛剛上市;整本書都重寫過; 今天就去買一本吧!)

好吧。

有點失焦了。我最好回顧一下到目前為止的內容:

找出能讓錯誤程式看起來錯的編程規範。讓正確的資訊集中在程式碼中相同的地方,方便你看出某些問題並立即修正。

我是匈牙利

Lugnano.JPG

我們現在回到惡名昭彰的匈牙利命名法。

匈牙利命名法是微軟程式設計師Charles Simonyi發明的。Simonyi在微軟做的主要計劃是Word;事實上他還主持了世界上第一個所見即所得的文書處理器(在Xerox Parc名為Bravo計劃)。

在所見即所得的文書處理中會用到可捲動的視窗,所以座標值有兩種意義:相對於視窗或相對於處理頁。兩種座標的差異很大,所以好好安排是非常重要的。

我猜這正是Simonyi開始採用某些之後被稱作匈牙利命名法的原因之一。它看起來像匈牙利文,而Simonyi是從匈牙利來,所以以匈牙利為名。在Simonyi版本的匈牙利命名法中,每個變數都會加一個小寫的字首,表示變數內容的種類。

打個比方,如果變數名為rwCol,rw就是字首hungarian.png

我是故意用種類(kind)這個詞,因為Simonyi在他的文章中誤用了型別(type),結果好幾世代的程式師都誤解了他的意思。

如果你仔細讀Simonyi的文章,就會發現他所講的和我之前範例所用的命名規範是一樣的,在我的範例中把uss分別定義為不安全字串和安全字串。這兩者的型別都是字串。如果你把某種字串指派另一種,編譯器並不會給任何警告,Intellisense也不會說些什麼。可是他們的語意是不同的;他們解讀和處理的方式都不同,要把兩種字串互相指派時還要某些轉換函數做轉換,否則就會有執行時期的問題。你好運。

微軟內部稱Simonyi對匈牙利命名法的原始概念為應用匈牙利命名法,因為它用於應用程式部門,也就是Word及Excel。在Excel的原始程式碼裡有大量的rwcol,你看到這些字首就知道它們指的是行(row)和列(column)。沒錯,它們都是整數,可是兩者間的轉換完全沒有意義。有人告訴我說Word的程式碼裡有大量的xlxwxl代表相對於排版頁面的水平座標,而 xw則代表相對視窗的水平座標。兩者都是整數但卻是不能互轉的。兩個程式裡都有很多cb,意思是位元組的個數。沒錯,這也是整數型別,不過光看變數名就可以得到更多資訊:這是位元組的個數,也就是緩衝區的大小。另外如果你看到xl = cb就可以拉警報了。這顯然是錯的程式,雖然xlcb都是整數,可是把以像素為單位的水平位移設成位元組個數絕對是瘋了。

在應用匈牙利命名法中字首可以用於函數和變數。因此雖然我真的沒看過Word的原始碼,我還是敢打賭Word裡一定有個叫YlFromYw的函數,可以把垂直方向的視窗座標轉成垂直方向的排版頁座標。應用匈牙利命名法用TypeFromType取代傳統的 TypeToType,這樣每個函數名就會以傳回的型別開頭,這正與我稍早在範例中把Encode改名為SFromUs的作法相同。事實上在正規的應用匈牙利命名法中Encode函數一定要改名為SFromUs。應用匈牙利命名法在該函數命名上並沒有提供其他選擇。這其實是件好事,因為你少一件事要背,另外也不必擔心Encode究竟是用什麼型別。程式也變得精確多了。

應用匈牙利命名法非常有用,特別是當初C語言盛行,而編譯器尚未提供很有用的型別系統時。

不過接下來卻出了一些問題。

黑暗世界占用了匈牙利命名法。

似乎沒有人知道為什麼或是如何發生的,不過似乎是視窗團隊中寫文件的人不小心創造出後來名為系統匈牙利命名法的東西。

某處有人讀了Simonyi的文章看到裡面用了「型別」這個字眼,因此認為作者指的就是型別,意思就像是類別或是型別系統中,或是編譯器所做的型別檢查。 其實不然。作者很小心並精確的解釋他用「型別」這個字的意義,不過沒有用。傷害已經造成了。

應用匈牙利命名法的字首很有用而且有意義,"ix"表示陣列索引,"c"表示個數,"d"表示兩個數字間的差(比如"dx"表示「寬度」),如此類推。

系統匈牙利命名法的字首作用就差多了,"l"表示長整數,"ul"表示正長整數而"dw"代表雙字組(呃,事實上就是正長整數)。在系統匈牙利命名法中,字首只能告訴你變數真正的資料型別。

這誤解了Simonyi的意圖和實作,差異雖細微實質上卻是完全不同。這件事唯一的教訓是讓你知道,如果你寫出些沒人能懂的艱深難解學術文章,你的想法可能會一再被誤解,結果變得非常荒謬,完全違背你的原意。所以在系統匈牙利命名法中會出現大量的dwFoo表示「雙字組的某某」,可惡的是某個變數是雙字組這件事對你幾乎是完全沒用的。難怪大家都很討厭系統匈牙利命名法。

系統匈牙利命名法的流傳既深又廣;它是整個視窗程式設計文件的標準;Charles Petzold的視窗程式設計(學習視窗程式設計的聖經)等書籍更為它廣為宣揚,很快的它也成為匈牙利命名法的主要勢力,即使在微軟內部也一樣。在微軟內也只有少數不在Word和Excel團隊的程式師瞭解他們搞出什麼樣的錯。

接下來就是大反抗了。有群程式師們從一開始就沒搞懂過匈牙利命名法,他們發現自己用的竟是煩人又幾近無用的分支,於是就起來反抗。不過系統匈牙利命名法裡還是有些好東西可以幫你看出問題。如果用系統匈牙利命名法,至少會在使用時知道變數型別。不過沒應用匈牙利命名法那麼有價值就是了。

大反抗在.NET第一版發行時到達巔峰,那時微軟終於告訴大家「不建議使用匈牙利命名法」。這還真是歡聲雷動啊。我根本不認為微軟會花心思解釋原因。他們只是掃瞄文件中命名指引的章節然後加上「不要使用匈牙利命名法」的字句。當時匈牙利命名法非常不受歡迎所以沒有人會真的抱怨,而除Excel及Word以外的人都因為不必再用這麼麻煩的命名規範而鬆了一口氣,他們認為在有強型別檢查及Intellisense的時代也不需要這種規範。

不過應用匈牙利命名法還是很有價值的,它加強了程式碼的連結讓程式碼更易閱讀,撰寫,除錯及維護,最重要的是它讓錯誤的程式看得出錯。

在繼續之前還有一件事我說過要做,就是再罵一次例外處理。我上次這樣做惹來很多麻煩。我在周思博趣談軟體首頁上一篇即興的評論中說我不喜歡例外處理,因為它實際上就是隱藏的goto,我認為這比看得到的goto更糟糕。當然就有幾百萬人跑出來痛罵我。全世界唯一跳出來替我辯護的當然也就是Raymond Chen。順帶一提,他既然是世界上最好的程式師,當然得出來講講話,對嗎?

這篇文章講到例外處理的重點了。你的眼睛學著看到錯誤的程式碼,這樣就能防止問題發生。為了讓程式能變得真正穩固,進行程式碼檢視時得有一套能集中資訊的命名規範。換而言之,你眼前有關程式運作的資訊愈多,尋找錯誤的結果愈好。當你看到以下的程式碼時

dosomething();
cleanup();

...你的眼睛會說沒什麼問題啊。我們總是要做清除的動作!不過dosomething有可能會引發一個例外,所以有可能不會呼叫cleanup。用finally等很簡單就能修正這個問題,不過這並不是我的重點:問題在於要知道cleanup一定會被呼叫到的唯一方法,就是調查整個dosomething呼叫樹,看看是否有任何場合會產生例外。這也還好,可控制式例外處理(checked exception)可以讓你不用那麼辛苦,不過重點是例外處理把資訊分散開來了。你得去看其他地方才能知道程式能正確執行,所以無法運用你眼睛天賦的功能去學習看出錯的程式碼,因為根本沒東西可看。

如果我寫個小腳本程式,只是每天一次到處收集資料然後印出來,這時候例外處理好用得不得了。我只想忽略所有可能出錯的地方,直接把整個程式用一個大try/catch包起來,如果有出什麼問題就用catch把錯誤電郵給自己。例外處理對簡單隨便寫的程式很有用,對腳本程式或是不是非常重要或無關生死的程式也不錯。不過如果你在寫一套作業系統或核電廠程式,或是用於開心手術的高速電鋸,例外處理可是危險的很。

我知道大家會認為我是個無法正確理解例外處理的笨程式師,完全不知道只有當我衷心接納例外處理後它才能改善我的生活。這種想法真是太糟糕了。想要寫出真正可信賴的程式碼,應該要嘗試用考慮到人有弱點的簡單工具,而不是靠那些提供有問題的抽象並把副作用隱藏起來,還認為程式師絕不出錯的複雜工具。

補充讀物

如果你還是衷心於例外處理,讀讀Raymond Chen的文章更乾淨更優雅,不過更難讀。「例外處理用得正確與否,很難由程式碼看得出來... 例外處理太難了,我實在不夠聰明無法掌握。」

Raymond對致命巨集的文章A rant against flow control macros討論了另一個讓資訊分散導致程式無法維護的例子。「當看到使用[巨集]的程式碼時,你必須看遍各個標頭檔才能瞭解它們的作用。」

想要瞭解匈牙利命名法的歷史背景,可以由Simonyi的原文匈牙利命名法開始。Doug Klunder在另一篇比較清楚的文章中把它引進Excel團體 。想知道更多匈牙利命名法的故事以及如何被文件撰寫人破壞的始末,可以去看Larry Osterman站上的貼文,特別是Scott Ludwig的評論,或是Rick Schaut貼的文章

我不寫軟體文章時就在製作FogBugz:一套名字笨笨的聰明專案管理軟體。現在就去看看(還有免費線上試用)。我們才剛推出大升級版FogBugz 4.0!


這些網頁的內容為表達個人意見。
All contents Copyright © 1999-2005 by Joel Spolsky。All Rights Reserved。


Personal tools