Saturday, March 7, 2009

打造更穩固的程式碼先從思路方向著手

Jan 15, 2009
打造更穩固的程式碼先從思路方向著手from 獨孤木 by qing

對程式員來說,撰寫程式的目標就是要完成功能,不論你的任務是要撰寫一整個系統、一個程式庫、一個模組、還是一個函式,不論你要撰寫的程式碼單位是什麼,在撰寫時,你總會有個功能性的目標,希望所完成的程式碼能夠提供特定的功能。而對初學程式設計的程式員來說,撰寫程式碼時,不可避免的會將所有的焦點以及心力放在達成功能,畢竟這是最起碼的要求,倘若連功能都不能完全實作出來,那麼更甭提其他事情了。



隨著經驗漸漸增多、技巧漸漸純熟,在撰寫程式碼時,所會關心的事情,慢慢的就會不僅僅限於如何實作出功能。你會關心能不能讓程式碼執行的更有效率,你也會關心如何讓程式碼更具可讀性、更容易維護,你或許還會關心如何讓程式碼容易修改、容易擴充。這些事情都是在滿足基本的溫飽(達成功能)以外的額外考量。



上述的幾個例子,都是許多初學者會漸漸涉獵到的進階議題。此外,或許有一個主題,是比較少會被探討到的,但我相信它的重要性,不在上述的這幾個進階議題之下。這個主題就是如何打造更穩固的程式碼。



所謂程式碼的穩固性(robustness),意指程式碼進到一個非常態的執行狀態時,能否適宜的因應。此處所指的非常態之執行狀態,有可能是資料庫連線無法建立,有可能是無法開檔或寫檔,有可能是所接收到的一個變數指標為NULL、等等…。而所謂適宜的因應,或許是平和的終止系統並留下清楚的訊息以指出所遭遇到的異常狀態,或許是停止繼續執行並回傳相對應的錯誤代碼,也有可能是嘗試著修復所遭遇到的異常情境,使其再度回到正常的狀態。



當程式員投注所有的心力試著要達成程式的功能時,他的焦點幾乎都會放在程式的正常狀態。一般來說,我們會將所預期的正常執行路徑稱為晴天情節(sunny day scenario),而異常的執行路徑則稱為雨天情節(rainy day scenario)。當我們只關心如何達成程式的功能時,我們幾乎就只考慮到它的晴天情節,也就是假設所有的條件、參數、以及狀態,都落在我們預期的理想範圍中。



不知道你有沒有過這樣子的經驗,有些程式員總是能交出看起來似乎能夠運作的程式碼,倘若針對他的程式碼做一些簡單的測試,他的程式碼看起來就像是依照規格般的將功能實作完畢。但是,倘若將這段程式碼放到雨天情節下執行,你會發現它不僅不能正常的反應,有時甚至會讓整個系統無預警的終止(最常見的,莫過於存取空指標所造成的記憶體違規存取動作了)。



或許剛入門時,你會覺得光是在晴天情節下達成功能就是一件不簡單的任務。但是,當你的實作經驗愈多,你會發現,如何在各式各樣的雨天情節下讓系統生存下去,其難度相形之下更高。各式各樣的雨天情節,正是突顯出程式員間功力高下的關鍵因素。優秀的程式員所寫下的程式碼,在達成功能的同時,也一併考慮一些可能遭遇到的異常情境,並且適度的加以處理,不致於放任系統進入絲毫不可預期的狀態,並且做出不可預期的行為,甚至是無預警的墜毀。



這中間不僅僅只是技巧面上的差別,更重要的是思路方向上的完全扭轉。多年前,我曾寫過一篇名為「未慮對,先慮錯」的文章,便是在探討這種截然不同的思路方向。在這篇文章中提到,下棋的人其思考習慣是「未慮勝,先慮敗」,也就是在落子前還沒想到勝過對手的情境,卻已經把各種可能的失敗情況思慮透徹。撰寫程式碼時也是一樣,「未慮對,先慮錯」代表在你的思路中,對雨天情節的考量更在正常情況下的晴天情節之前。



有許多程式員處理一個物件時,不會考慮到這個物件可能是NULL的情況;當他們拿到一個陣列的索引值時,不會考慮到這個索引值可能超出陣列可允許的存取範圍;當他們開啟一個檔案時,不會考慮到這個檔案或許不存在;當他們試著建立一個網路連線時,不會考慮到連線可能會建立失敗;他們甚至不會取得他們所呼叫的大多數函式的回傳值,以進一步檢查這些函式的叫用動作是否失敗。因為他們的焦點完全就只放在最理想的正常執行路徑上。不過,天氣總不是天天都放晴,有時多雲有時陣雨,有時甚至還會颳颱風。



只考慮晴天情節的程式碼若是埋藏在系統中,像是隨時都可能被引爆的未爆彈。倘若測試案例的涵蓋範圍不夠大,那麼便有可能無法查覺潛在的問題。一旦遇上了下雨天,系統就要出事了。這樣的程式碼大大的降低了系統的穩固性。



當你撰寫程式碼的態度是「未慮對,先慮錯」時,基本上是以懷疑與不信任的眼光來看待你所面對的一切。對於每個所面對的變數,你會先在心中琢磨,它們的值是不是有可能是非法的值。對於每次的函式叫用動作,你都會假設它們有可能執行失敗。你甚至會考慮到,其他人所提供的函式或許還實作錯誤。當你實作你自己的函式時,你會假設使用你函式的客戶端程式員有可能根本就是胡亂使用,他所傳進函式的引數完全都是非法的值。當你實作與使用者相接的程式時,你會假設使用者根本就是毫無章理地胡亂輸入,或許你還假設使用者有可能是隻猴子。



近年來,有許多人(例如極限編程XP的愛好者)主張採用測試先行的開發方式。也就是說,在撰寫真正的程式碼前,先寫好測試這段程式碼的測試案例。事實上,這便是一種「未慮對,先慮錯」思考習慣的展現。撰寫測試案例和撰寫程式碼時會抱持的心態恰好相反。撰寫程式碼時容易正向思考,偏向思考如何在正常情況下達成功能。但撰寫測試案例時,腦子想的卻淨是如何提供各種程式可能沒有考慮到的情境條件。當你採用測試先行的開發方式時,會在撰寫真正的程式碼之前,仔細的在腦中將可能的各種情況一一展開。那麼,當你真正進到撰寫程式碼的階段時,自然而然的會將更多的可能情況納入考量並且加以處理。



有許多程式員會利用所謂的assertion(維護)來檢查程式所能處理的執行狀態。在某些語言中,它是以函式庫的方式實作,有些語言下它是巨集,而像在Java程式語言中,甚至提供了專門的算式語法。不論究竟是以何種方式實作assertion,其目的都是讓函式的撰寫者檢查函式執行時的狀態是否符合預期,例如檢查所傳入的某一指標是否不為NULL。雖然assertion僅在開發除錯階段才會起作用,並且被視為是一種開發時除錯的有用工具。而在正式的釋出版本中並不含assertion的機制,但是對程式員而言,養成在程式碼中運用assertion的習慣,意謂著你在撰寫程式碼的同時,總是思考了你所撰寫的程式碼所能夠因應的執行狀態,並且明白的表明了你的程式碼所不能接受的執行狀態。而這種思維方式,自然有助於你將思考的範圍擴展的更為廣泛,因而更有可能寫出較為穩固的程

No comments: