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對流行的設計方法的評論


 


設計沒有一定的標準,無法定義需要做多少設計才足夠。


 


「設計一切」與「不做設計」都是極端不正確的作法。


 


把設計看成探索過程,不要停留在第一套解決方案、尋求合作、追求簡潔性,需要時做出原型,並進一步迭代。


 


核對表:軟體建構中的設計


設計實踐


 


多次迭代?


多種方案分解系統?


同時自上而下與自下而上設計?


對系統風險或不熟悉部分建立原型?


設計方案被人檢查過?


細節實作?


有保留設計成果?


 


設計目標


 


系統架構層定義


設計劃分層次


程式分解方式


類別分解方式


類別之間互動關係最小化


類別與子程式能否重用


易於維護


精簡


標準技術


最小化複雜度


 


要點


 


軟體首要使命是「管理複雜度」,以簡單作為努力目標。


簡單可用兩種方法,減少同一時間關注的複雜度數量與避免不必要的附加複雜度


設計是啟發過程,避免只使用一種方法


好的設計都是迭代嘗試的結果


資訊隱藏可解決很多困難的設計問題


 


 


留言

這個網誌中的熱門文章

概說H-Game

AOC和人生

真珠美人魚 星羅「Beautiful Wish」中文版