CODE COMPLETE 2 軟體開發實務指南 第5章 軟體建構中的設計
第五章的主題是軟體設計,內容可幫助讀者「開竅」。子曰:「吾道一以貫之。」軟體設計有同樣的情況,軟體設計的本源是化繁為簡降低複雜度,衍生出抽象、封裝、繼承與多型。讀者若是能夠看懂第五章,表示做到融會貫通了。
5.1 設計中的挑戰
軟體設計代表去構思、創造或發明一套方案,把電腦軟體的規格書,轉變為實際可行的軟體。
設計是把需求分析和編碼除錯連結在一起的活動。
好的高層設計能容納多的較低層次設計的結構。
設計是一個險惡的問題
學校寫作業老師給的題目不會改變,出社會工作面對實際情況,客戶的需求總是會一直改變。
設計是個毫無章法的過程(即使它能得出清爽的成果)
設計的過程是持續犯錯的過程,設計的好壞沒有一定的標準。
設計就是確定取捨和調整順序的過程
設計要根據現況進行調整。
設計受到諸多限制
設計一部分在創造,一部分又是在限制可能發生的事情。
設計是不確定的
人各有體,三個人有三種不同的設計。
設計程序本質上是個啟發式的程序
設計是一種啟示,根據經驗法則或嘗試說不定可行的辦法。沒有工具是一直都可以使用,不需要改變。
設計是自然而然形成的
設計是不斷地設計評估、非正式討論、寫試驗程式、修改試驗程式碼。
5.2 關鍵的設計概念
軟體的首要技術使命:管理複雜度
附屬的和本質上的難題(Accidental and Essential Difficulties)
語言、效能和開發環境屬於附屬問題。
細節、完全正確、實體間的互動屬於本質問題。
管理複雜度的重要性
軟體的首要技術使命就是管理複雜度。
可以透過把整個系統分解為多個子系統來降低問題的複雜度。軟體設計的目標都是將複雜的問題分解為簡單的問題。
子系統之間相互依賴越少,越容易專注問題的一小部分。
如何處理複雜度問題
高代價、低效率的設計來自於下面三種來源
用複雜的方法解決簡單的問題
用簡單但錯誤的方法解決複雜的問題
用不恰當的複雜方法解決複雜的問題
要用下面兩種方法管理複雜度
本質複雜度的量減到最少
不要讓附屬的複雜度無謂地快速增長
理想的設計特徵
最小的複雜度
易於維護
鬆散耦合
可擴充性
可重用性
高扇入 大量的類別使用某個特定的類別
低扇出 一個類別少量或適中地使用其他的類別
可移植性
精簡性
層次性
假設存在舊的程式碼,層次化有兩個好處。將低劣程式碼拘禁起來、如果最後移除舊程式碼,不用修改其他層次的程式碼。
標準技術 儘量使用標準化、常用的方法。
設計的層次
第1層:軟體系統
作者認為從子系統或套件思考會比較有益。
第2層:分解為子系統或套件
識別出所有主要的子系統,定義個子系統如何使用其他的子系統。子系統之間要限制通訊,每個子系統才有存在的意義。
重要的問題
開發人員需要理解系統中多少不同的部分,才能在圖形子系統更動某些東西?
想在另一個系統中試圖使用業務規則,會發生什麼事?
加入新的使用者介面會發生什麼?
把資料儲存到一台遠端機器上,又會發生什麼?
子系統之間要避免環形依賴。
常用的子系統
業務規則
電腦系統中編入的法律、規則、政策及程序。
使用者介面
使用者介面元件與其他部分隔離開來,讓使用者介面的演化不會破壞其餘程式。
資料庫存取
對資料庫存取的實作應該隱藏起來,讓其他程式像在業務層次處理資料。
對系統的依賴性
對作業系統的依賴歸到一個子系統,有利於可移植性。
第3層:分解為類別
識別出系統所有的類別。
類別與物件的比較
類別就像是設計圖,物件是程式中實際存在的實體。
第4層:分解成子程式
把類別再細分為子程式。
第5層:子程式內部的設計
寫每個子程式內部詳細的功能。
5.3 設計構造塊:啟發式方法
找出現實世界中的物件
首選物件導向設計方法,要點是辨識現實世界中的物件與人造的物件。
使用物件進行設計的步驟是:
辨識物件及其屬性,屬性代表物件的方法和資料
確定可以對各個物件進行的操作
確定個各個物件能對其他物件進行操作
確定物件哪些部分是公用,哪些部分是私用。
定義每個物件的公開介面
這些步驟經常會反覆執行,Iteration迭代是非常重要的。
有兩種方法迭代
高層次的系統組織結構上進行迭代,以便獲得更好的類別組織結構
在以定義好的類別上進行迭代,設計每個類別進入更細節的層次
形成一致的抽象
抽象是一種簡化,稱呼房屋或是城鎮就是在使用抽象。
基底類別是一種抽象,集中精力關心衍生類別共同具有的特性,並在基底類別的層次上忽略具體衍生類別的細節。
抽象的好處在於能夠忽略無關的細節。
封裝實作細節
封裝彌補抽象留下的空白。看圖5-7與圖5-8就能夠明白。
圖5-7 抽象可以讓你用一種簡化的觀點來考慮複雜的概念。
圖5-8 封裝不讓你看到複雜概念的任何細節,你只能看到你能看到的部分。
當繼承能簡化設計時就繼承
出現一些大同小異的物件可以考慮用繼承化簡。定義物件之間的相同點和不同點叫做繼承。
有繼承才有多型,在執行期間才能確定物件的實際類型。
隱藏秘密(資訊隱藏)
結構化設計中的黑盒子概念來自資訊隱藏,物件導向設計中資訊隱藏又引出封裝和模組化的概念。
資訊隱藏是重要的啟發性方法,強調隱藏複雜度。
秘密和隱私權
在設計一個類別時,關鍵性決策確定類別的哪些部分對外可見,哪些特性應該被隱藏起來。
隱藏的秘密可能是某個易變的區域、某種檔案格式、某種資料類型的實作方式,某個需要被隔離的區域。
資訊隱藏的一個例子
????????????????????????
兩種秘密
隱藏複雜度
隱藏變化源
資訊隱藏的障礙
資訊過度分散
100數字出現在程式各處,可以將100寫入MAX_EMPLOYEES常數,只要更動一個常數就可以同時更動多處。
系統內部都有與GUI介面互動的內容,GUI介面改變會造成全部都要改,最好的方式是將人機互動邏輯集中到一個單獨類別、套件、子系統。
一個全域範圍1000個元素的員工資料陣列有可能到處分散,透過存取子程式來使用陣列,就只有存取子程式才知道實作的細節。
循環依賴
類別A使用類別B的子程式,類別B又使用類別A的子程式。要避免循環依賴,會讓系統不容易測試。
把類別的資料誤當成是全域資料
全域資料會出現多個子程式存取同一個全域資料的情況,類別內部只有少數子程式才能存取類別資料。
可以察覺的效能損耗
部分程式設計師擔心會拖慢速度,而放棄資訊隱藏。
資訊隱藏的價值
程式修改容易、資訊隱藏觀念補助物件導向程式設計,有助於設計類別公開介面。
作者建議要思考「我該隱藏些什麼?」
找出容易改變的區域
優秀的程式設計師都有對變化的預期能力,要把不穩定的區域隔離,將變化帶來的影響限制在一個子程式。
1.找出看起來容易變化的項目
2.把看起來容易變化的項目給分離出來
3.把看起來容易變化的項目隔離開來
找出->分離->隔離
容易發生變化的區域
業務規則
對硬體的依賴性
輸入和輸出
非標準的語言特性
例如使用了特殊環境才能運行的子程式庫。
困難的設計區域和建構區域
困難的程式可能會寫得不夠好,將來有很大的機率還會更改。
狀態變數
不要用布林變數做為狀態變數,要用列舉類型。
使用存取子程式取代對狀態變數直接檢查,寫測試程式會比較容易,在一個子程式內就可以做到,不用到處都寫測試程式。
資料量的限制
程式參數經常都會改變。
預料不同程度的變化
如果一種變化很可能發生,就要確保系統能夠接受接納這個變化。
保持鬆散耦合
耦合度表示類別與類別、子程式與子程式之間的緊密程度。
耦合度設計的目標是要建立小的、直接的、清晰的類別或子程式。
儘量使模組不依賴或很少依賴其他模組。
耦合標準
規模
模組之間的連接數。只有一個參數的子程式,比有六個參數的子程式耦合更加鬆散。
可見性
兩個模組之間的連接顯著程度。透過參數列表傳遞參數是一種明顯連接,透過全域變數傳遞資料是偷偷摸摸的做法,將全域變數文件化又是稍微好一些的做法。
靈活性
模組之間的連接是否容易更動。舉例來說正常人會喜歡USB連接器而不是直接焊死。
一個模組越容易被其他模組呼叫,它們之間的耦合關係就會越鬆散。
耦合的種類
簡單資料參數耦合
兩個模組透過簡單參數傳遞資料。
簡單物件耦合
一個模組實例化一個物件。
物件參數耦合
兩個模組透過物件傳遞資料。例如Object1 要求Object2傳遞Object3給它,這種耦合關係要更緊密一些。
語義上的耦合
最緊密的耦合,一個模組不僅使用另一個語法元素,而且還使用模組內部工作細節語義知識。
底下是一些例子:
Module1向Module2傳遞一個控制標誌,命令Module2應該做什麼事。
Module2在Module1修改了某全域資料之後,使用該全域資料。
Module2實例化Module1之後,只呼叫Module1.Routine(),沒有呼叫Module1.Initialize。
Module1將Object傳給Module2,只有將Object部分初始化。
Module1將BaseObject傳給Module2,Module2呼叫DerivedObject的特有方法。
語義上的耦合非常危險,有可能編譯器無法察覺錯誤。
類別和子程式是用於降低複雜度的首選。
查閱常用的設計模式
設計模式透過提供現成的抽象來減少複雜度
不需要一行一行講解,別的程式設計師就能夠瞭解程式碼中的設計思維。
設計模式透過把常見解決方案的細節制度化來減少出錯
設計模式是前人經驗與智慧的累積。
設計模式透過提供多種設計方案帶來啟發性的價值
設計模式讓程式設計師容易找到解決問題的方法。
設計模式把設計對話提升到一個更高的層次來簡化交流
設計模式簡化程式設計師之間的交流。
表5-1列出常見的設計模式
要注意不可以誤用設計模式,也不可以為了模式而模式。
其他的啟發式方法
高內聚力
內聚力是類別內部所有子程式,或子程式內程式碼支援一個中心目標時的緊閉程度。
子程式越是使用同一個類別的變數或方法表示內聚力越高。
建構分層結構
抽象的概念表示位於層次關係的最上層,實作是最下層。
恪守類別契約
類別的客戶向該類別所作出的承諾稱為前置條件,物件向其客戶所做的承諾稱為後製條件。
分配職責
每個物件該對什麼負責,這個物件應該隱藏些什麼資訊
為測試而設計
測試驅動開發
避免失誤
要研究成功案例,也要關心失敗案例。
有意識地選擇綁定時間
綁定時間(Binding time)是把特定值綁定(寫入)到某一變數的時間。早綁定會比較簡單,但也缺乏靈活性。
建立中央控制點
控制可以被集中在類別、子程式、前置處理器及#include檔案裡,常數也有可能是中央控制點。
需要搜尋的地方越少,修改起來會越容易。
考慮使用蠻力突破
循序搜尋法解決問題。
畫一張圖
一圖能夠抵千言。
保持設計的模組化
模組化的目標是使得每個子程式或類別看起來像是個黑盒子。
關於設計啟發的總結
尋找現實事件的物件
形成一致的抽象
封裝實作細節
在可能的情況下使用繼承
藏住秘密資訊隱藏
找出容易變動的區域
保持鬆散耦合
探尋通用的設計模式
使用啟發式方法的原則
5.4 設計實踐
迭代(Iterate)
設計是來來回回重複的過程。
分而治之(Divide and Conquer)
把程式分解為不同的關注區域,然後分別處理每一個區域。
增量式改進:理解問題、形成計畫、執行計畫,回顧作法。
自上而下和自下而上的設計方法
自上而下設計從高層抽象開始,自下而上設計始於細節。
自上而下的論據
從一般類別出發,一步步分解為更具體的類別,大腦就不會處理過多的細節。
自下而上的論據
找出低層職責,會感覺到從頂層觀察系統一次舒服多了。
部分情況主要屬性由底層決定,可能需要與硬體打交道,介面需求決定設計一大部分。
自下而上合成時,需要考慮以下因素:
系統需要做的事項
找出具體的物件和職責
找出通用物件,按照適當方式組織起來
往上一層工作,或是回到最上層嘗試向下設計
其實沒有爭議
分解策略和合成策略的差別。
自上而下的優點簡單、延後建構細節。
自下而上的優點能夠更早找出所需功能,缺點在於較難執行,大多數人擅長大概念分成小概念,或是發現無法組合出想要的系統。
兩種設計方法不是互斥關係,可以同時進行。
建立試驗性原型
建立原型是寫出用於回答特定問題的、數量最少且隨時可拋棄的程式碼。
感覺就是在寫測試程式。
合作設計
合作可以用下面任意一種方式進行
到同事辦公桌前討論
會議室畫白板討論
結對程式設計
報告自己的設計想法
正式檢查
經過一個星期,再來回顧。
網路討論區提問
要做多少設計才夠
記錄你的設計成果
把設計文件插入到程式碼裡
用Wiki來記錄設計討論和決策
寫總結郵件
使用數位相機
保留設計掛圖
使用CRC卡片
在適當的細節層建立UML圖
5.5對流行的設計方法的評論
設計沒有一定的標準,無法定義需要做多少設計才足夠。
「設計一切」與「不做設計」都是極端不正確的作法。
把設計看成探索過程,不要停留在第一套解決方案、尋求合作、追求簡潔性,需要時做出原型,並進一步迭代。
核對表:軟體建構中的設計
設計實踐
多次迭代?
多種方案分解系統?
同時自上而下與自下而上設計?
對系統風險或不熟悉部分建立原型?
設計方案被人檢查過?
細節實作?
有保留設計成果?
設計目標
系統架構層定義
設計劃分層次
程式分解方式
類別分解方式
類別之間互動關係最小化
類別與子程式能否重用
易於維護
精簡
標準技術
最小化複雜度
要點
軟體首要使命是「管理複雜度」,以簡單作為努力目標。
簡單可用兩種方法,減少同一時間關注的複雜度數量與避免不必要的附加複雜度
設計是啟發過程,避免只使用一種方法
好的設計都是迭代嘗試的結果
資訊隱藏可解決很多困難的設計問題
留言
張貼留言