讀書筆記:A Philosophy of Software Design
今天一位同事在斯坦福的博士生導師John Ousterhout (注,Tcl語言的設計者)來公司做了他的新書《A Philosophy of Software Design》的演講,介紹了他對於軟體設計的思考。這裡我把本書的讀書筆記和心得分享給大家,歡迎大家來和我交流探討。
大家也可以去看作者在google演講時的視訊和他演講的slides
複雜性的本質
軟體設計應該簡單,避免複雜,關於複雜性的定義,作者認為主要有兩個量度
1. 系統是不是難以理解
2. 系統是不是難以修改
關於複雜性的症狀:
1. 當新增特性時,需要修改大量的程式碼
2. 當需要完成一個功能時,開發人員需要了解許多知識
3. 當新增/修改功能時,不能明顯的知道要修改那些程式碼
引起復雜性的原因:依賴和晦澀。
最後,複雜性不是突然出現的,它是隨著時間和系統的演進逐漸增加的。
我的解讀:這本書講的是軟體設計的哲學,哲學要解決的是最根本的問題。作者認為軟體設計要解決的最根本的問題就是避免複雜性,依賴和晦澀是造成軟體負責的主要原因。依賴很多時候是無法避免的,但是應該儘可能的減少依賴,去除不必要的依賴。軟體設計應該容易理解,晦澀是引起復雜性增加的另一個原因。這個核心觀點是這本書的主旨,借用老愛的話"Simple,but not simpler!"
我曾經就職某儲存巨頭,其中有一塊程式碼因為是收購的產品,程式碼已經非常陳舊了,因為沒有人能看懂,所以也就沒有人敢修改。你看,這個產品不是也賣的挺好的。
僅僅可工作的程式碼還遠遠不夠
在第二章,作者提出了"戰術性程式設計"和"戰略性程式設計"的對立。
"戰術性程式設計"最求以最快的速度完成可工作的功能。這看上去無可厚非。但是這種行為往往會增加系統的複雜性。引發大量的技術債。可以說這種做法以犧牲長遠利益來獲得眼前的利益。
"戰略性程式設計"不僅僅要求可工作的程式碼,以好的設計為重,未來的功能投資,認為現階段在設計上的投入會在將來獲得回報。
好的設計是有代價的,問題是你願意投入多少?
我的解讀:很有趣的是,我司之前的產品的負責人在公司推行大規模的敏捷(LeSS),當時有一個顧問給我們上課,他也說設計要儘可能簡單,但是不要為了未來做設計。以最小的代價實現可用的功能。以John的觀點,這樣做無疑會增加系統變複雜的可能性。我比較認同John這裡的觀點,好的設計是有價值的,投入在軟體設計上的,對功能毫無影響的東西,是有價值的。但是如何取捨和權衡,投入多少是需要開發團隊達成共識。 軟體有它的生命週期,為了未來的投入也不是越多越好。
模組要有深度
深度其實是對模組封裝的度量,模組應該提供儘可能簡單的介面和儘可能強大的功能。這樣的模組稱之為深度模組。
我的解讀:這一部分沒有什麼新東西,傳統的面向物件和如今的微服務架構都是對這一哲學的應用。好的封裝可以減少依賴,簡單的介面可以避免晦澀。也就是減少了複雜性。
資訊的隱藏和洩漏
關於資訊的隱藏和洩漏,這一部分對於熟悉面向物件的猿們來說不是新東西。基於SOLID,這就是Open,軟體應該是對於擴充套件開放的,但是對於修改封閉的。資訊隱藏使得修改變的封閉。
具有通用功能的模組更具深度
更通用功能的介面意味著更高層級的抽象,隱藏更多的實現細節,按照John的觀點,也就更具深度。那麼如何在通用介面和特殊介面之間做權衡呢?
1. 能夠實現所需功能的最簡單介面是什麼?
2. 該介面會被用於那些不同場景?
3. 該介面對於我的當前是否容易使用?
我的解讀:通用的介面和之前的"戰略性程式設計"是一致的,更通用的介面在面對未來可能發生的需求變化的時候,更容易使用。這裡的藝術在於能夠找到需求到軟體介面之間的最佳對映。抽象到哪一個層級,是主要問題。
不同的層,不同的抽象
軟體系統通常有不同的層次組成,每一層都通過和它之上和之下的層的介面來互動。每一層都具有自己不同的抽象。例如典型的資料庫,伺服器和客戶端模型中,資料庫層的抽象是資料表和關係,伺服器層是應用物件和應用邏輯而客戶端的抽象是使用者介面檢視和互動。如果你發現不同的層具有相同的抽象,那也許你的分層有問題。
把複雜性向下移
在軟體分層的鄙視鏈中,最高層是使用者,接著的一層的UI工程師,然後是後臺工程師,資料庫工程師,等等。使用者是上帝不能得罪,如果一定要在某個層次處理複雜性,那麼這個層次越低越好,反正苦逼程式設計師也不會抱怨,對得,就是這個道理。
合併還是分離
"天下大事,分久必合,合久必分"。軟體設計中經常要問的問題就是這兩個功能模組是合併好,還是分開好?不論是合併還是分離,目標都是降低複雜性,那麼把系統分離成更多的小的單元模組,每一個模組都更簡單,系統的複雜性會降低麼?答案是不一定:
ⷠ複雜性可能來源於系統模組的數量
ⷠ更多的模組也許意味著需要額外的程式碼來管理和協調
ⷠ更多的模組可能帶來許多依賴
ⷠ更多的模組可能帶來重複的程式碼,而重複的程式碼是惡魔
在以下的情況下,需要考慮合併:
ⷠ模組之間共享資訊
ⷠ合併後的介面更簡單
ⷠ合併後減少了重複的程式碼
確保錯誤終結
異常和錯誤處理是造成軟體複雜的罪魁禍首之一。程式設計師往往錯誤的認為處理和上報越多的錯誤,就越好。這也就導致了過度防禦性的程式設計。而很多時候,程式設計師捕獲了異常並不知道該如何處理,乾脆往上層扔,這就違背了封裝原則。
使用者一臉懵逼,"你叫我幹啥?"
降低複雜度的一個原則就是儘可能減少需要處理的異常可能性。而最佳實踐就是確保錯誤終結,例如刪除一個並不存在的檔案,與其上報檔案不存在的異常,不如什麼都不做。確保檔案不存在就好了,上層邏輯不但不會被影響,還會因為不需要處理額外的異常而變得簡單。
設計兩次
這裡"設計兩次"的意思是無論設計一個類,模組還是功能,在設計的時候仔細思考,除了當前的方案,還有那些其它的選擇。在眾多設計中比較,列出各自的優缺點,然後選出最佳方案。就是對於設計方案,都有兩個或者兩個以上的選擇。
對於大牛而言,也許設計方案顯而易見,於是覺得沒有必要在不同方案中做遴選。然而這並不是一個好的習慣,這說明,你沒有在處理更困難的問題,問題對於你而言太簡單了。這不是一個好的現象,因為上坡路總是很難走。當你面對困難的問題的時候,通過對不同設計方案的學習和思考,你會成長到更高的一個層次。
我的解讀:在管理理論上有一個叫彼得原理,就是"在一個等級制度中,每個人趨向於上升到他所不能勝任的地位"。程式設計師也面臨同樣的問題,當你的經驗和資歷不斷的提高,你總會遇到你所不能勝任的問題,這個時候就需要通過不斷的學習,提高自己。當然也有可能所處的環境無法給你更具挑戰的問題。這個時候你就需要考慮,你的下一站在哪裡?
為什麼要寫註釋
困擾程式設計師的兩大世界性難題:
1. 別人的程式碼沒有註釋
2. 別人讓我給我的程式碼寫註釋
程式設計師通常有各種理由不寫註釋:
1. 好的程式碼是自解釋的
2. 沒時間寫
3. 註釋很快就會和程式碼不一致,造成誤解
4. 我讀的其他人的註釋都毫無意義
我的解讀:其實開發過軟體的工程師都能理解寫註釋的重要性和意義,這並不需要很多的解釋。但是"懶惰"是原罪之一,我就是不想寫呀不想寫。
關於軟體開發的七宗罪,請閱讀AntiPatterns
註釋應當用於描述程式碼中不易理解的部分
如果你一定要對於顯而易見的部分增加註釋,那麼可能你是按程式碼行數收取工資吧,當然,註釋也是算行數的。
選擇命名
給變數,類,模組,檔案起名字很難,真的很難。好的命名能使得軟體設計更容易理解,差的命名更容易產生Bug。
我就被坑過。還是在某儲存公司的時候,負責開發一個軟體升級的規則模組,根據不同的規則決定能不能升級。當時我的程式碼release之後,發現客戶不能升級了。於是我們在程式碼中找Bug,後來發現,原因是我的程式碼判斷"hardware"欄位來決定目標硬體型別是否匹配,而應該是另一個和"hardware"命名很像的另一個欄位來決定要升級的硬體的型別。更糟糕的是,因為這個欄位實在是比真正應該判斷的欄位看上去更合理,進行程式碼審查的人都沒能看出這個問題。而當時沒有測試環境能夠實際匹配到這個硬體型別,這個問題也沒能在測試環節中發現。
註釋先行
在實現過程中,把介面和註釋先準備好。
修改現有程式碼
對於修改程式碼,同樣面臨著"戰術性程式設計"和"戰略性程式設計"的挑戰,是以最少的修改完成任務,還是以重新設計使得系統更合理的角度進行長線投資,需要仔細思考。
我的解讀:隨便改一些不相關的程式碼,你可能會發現Bug神奇的消失了,軟體開發需要運氣,祈禱有的時候真的管用。
一致性
一致性在軟體設計裡很重要,包括:
1. 命名
2. 程式碼風格
3. 介面
4. 設計模式
5. 常量
可以使用以下的方法來保證一致性:
1. 文件
2. 利用工具/程式碼審查來強制
3. 入鄉隨俗
4. 不要隨便改變命名約定
程式碼應當顯而易見
怎麼定義程式碼是不是顯而易見,就是帶程式碼審查的時候,如果有人認為這的程式碼不是容易理解,那麼這個程式碼應該就是有問題的。也許這個程式碼對你來說很直觀,但是程式碼不是寫給自己看的。應該讓團隊裡的其他成員也能讀懂你的程式碼。
有一些使的程式碼不易理解的元素:
1. 事件驅動模式 - 因為不知道事件流控制的順序
2. 範型 - 也許執行時才知道型別,造成閱讀的困難
我的解讀:最早曾在一家通訊企業做管理軟體開發,幾年後被要求修改自己多年前寫的程式碼,讀了好久,愣是沒看懂。
軟體開發的趨勢
John對軟體開發重的一些趨勢和問題做了總結:
1. 面向物件,對於繼承,基於介面的繼承要優於基於實現的繼承
2. 敏捷,敏捷的一個潛在問題是導致"戰術性程式設計"為主導,導致系統的複雜性增加
3. 單元測試
4. 測試驅動,測試驅動的問題是關注功能,而非找到最佳設計
5. 設計模式,設計模式的問題可能導致過度應用
6. Getter/Seeting, 這個模式可能是冗餘的,也許不如直接暴露成員更簡單
為效能做設計
關於如何在複雜性和效能之間的權衡,通常更簡單的程式碼執行的更快。當然很有可能更復雜和晦澀的程式碼效能更高,例如彙編對比Python。設計的時候需要考慮的是為了獲得性能的提升,代價是什麼?這樣的代價是不是值得?
在為了效能做出修改之前,先進行測量。針對關鍵路徑,找到影響效能的核心單元,做出效能改進的設計。
這本書的核心是關於"複雜性"的,軟體無疑是一個非常複雜的領域。對於導致複雜的原因,我覺得John的觀點沒有問題,但是實際上還有很多更深層的原因。軟體開發和人息息相關,離開人來講純軟體的東西,其實並不複雜,軟體開發中引起復雜性的更多原因是更為複雜的人,團隊,組織,和組織關係。這並不是對該書的否定,這本書對於程式設計師來說還是很好的一本書,值得一讀。