A Philosophy of SoftwareDesign

A Philosophy of SoftwareDesign

來自專欄剛是程序員8 人贊了文章

今天一位同事在斯坦福的博士生導師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的觀點沒有問題,但是實際上還有很多更深層的原因。軟體開發和人息息相關,離開人來講純軟體的東西,其實並不複雜,軟體開發中引起複雜性的更多原因是更為複雜的人,團隊,組織,和組織關係。這並不是對該書的否定,這本書對於程序員來說還是很好的一本書,值得一讀。


推薦閱讀:

一流大學研發大型複雜信息系統——方法與實踐2.0版:PDF版下載
團隊協助(1)-GIT入門
android...ActionBarOverlayLayout如何解決
軟體開發怎麼這麼貴,它的價格是怎麼定義的?
Magento2開發人員認證考試

TAG:軟體工程 | 軟體開發 |