CODE COMPLETE 2 軟體開發實務指南 第6章 工作類別

        第六章的主題是類別,可以與《Clean Code》第六章與第十章對照看。


類別是由一組資料和子程式構成的集合,擁有內聚性明確定義的職責。


6.1 類別的基礎:抽象資料類型 (ADTs)


首先考慮ADT,然後再考慮類別。


ADT讓人像在現實生活中操作實體,不用太在乎細節。


需要用到ADT的例子


不使用ADT就會變成要實際操作細節,例如字型大小12點,高度是16像素


currentFont.size = 16


字型設定為粗體


currentFont.attribute = currentFont.attribute or 0x02  或是 currentFont.bold = True


使用ADT的好處


可以隱藏實作細節


更動不會影響到整個程式


讓介面能夠提供更多資訊


更容易提高效能


讓程式的正確性更顯而易見


程式更具有自我說明性


無需在程式內到處傳資料


可以像在現實世界中那樣操作實體,而不用在底層實作上操作它


 


ADT範例


 


currentFont.SetSizeInPoints (sizeInPoints)


currentFont.SetSizeInPixels ( sizeInPixels)


currentFont.SetBoldOn()


currentFont.SetBoldOff()


currentFont.SetItalicOn()


currentFont.SetItalicOff()


currentFont.SetTypeFact (faceName)


 


主要區別在於對於字型的細節實際控制都隔離到一組子程式。給其他程式提供更好的抽象層,也提供一層保護。


更多的ADT範例


把常見的底層資料類型建立為ADT,使用這些ADT而不使用底層資料類型


把像檔案這樣的常用物件當成ADT


簡單的事物也可當做ADT


不要讓ADT依賴於它的儲存介質


例子讀取費率檔案,RateFile.Read()不夠抽象透露儲存檔案,改成rateTable.Read()或rates.Read()較好。


在非物件導向環境中用ADT處理多份資料的實例


C語言沒有類別,要多寫一些函數達到抽象。


ADT和類別


ADT是類別概念的基礎,在支援類別的語言中,可把每個ADT用它自己的類別實作。


6.2 良好的類別介面


可透過介面來展現良好的抽象,並確保細節被隱藏在抽象背後。


良好的抽象


抽象是一種以簡化的形式來看待複雜操作的能力。類別的介面為隱藏在其後的具體實作提供一種抽象。類別的介面應能提供一組明顯相關的子程式。


類別介面是類別所具有的公用子程式所構成的集合。


這段看範例比較清楚,不良抽象的類別介面包含大量混雜的函式。


類別的介面應該展現一致的抽象層次


這段同樣看範例會比較清楚。惡例誤用繼承,導致出現介面出現兩種抽象層次。良例改用組合。


一定要理解類別所實作的抽象是什麼


作者提供一個親身經歷,明顯雞同鴨講作者與工程師對於正確的抽象認知不同。


提供成對的服務


「有光必有影」有開燈就會有關燈,有增加元素就會有刪除元素。


把不相關的資訊轉移到其他類別中


盡可能讓介面可程式化,而不是表達語義


可程式化的部分編譯的時候可以檢查錯誤,語義(semantic)編譯的時候無法檢查錯誤,可用Assert轉程式化的方法檢查是否有錯。


謹防在修改時破壞介面的抽象


範例寫得清楚public的部分添加別的功能破壞了介面抽象。


不要添加與介面抽象不一致的公用成員


添加公用成員的時候要考慮這個子程式與現有介面的抽象是否一致?


同時考慮抽象性和內聚性


良好的封裝


抽象忽略實作細節的模型來管理複雜度,封裝強制阻止境外勢力看到細節。


抽象與封裝是共存的關係。


盡可能地限制類別和成員的可存取性


能放私有區域就儘量放在私有區域


不要公開暴露成員資料


暴露成員資料會破壞封裝,限制對抽象的控制能力。


避免把私用的實作細節放入類別的介面中


範例說明很清楚,私有區域暴露了細節,修改之後放在EmployeeImplementation類別。


不要對類別的使用者做出任何假設


避免使用友誼類別


友誼類別會破壞封裝。


不要因為一個子程式裡僅用公用子程式,就把它歸入公開介面


讓閱讀程式碼比編寫程式碼更方便


要格外警惕從語義上破壞封裝性


範例中呼叫程式碼不是依賴於類別的公開介面,而是依賴於類別的私用實作。


留意過於緊密的耦合關係


盡可能限制類別和成員的可存取性。


避免使用有友誼類別,因為它們是緊密耦耦合的。


在基底類別中把資料宣告為private而不是protected,以降低衍生類別和基底類別之間的耦合程度。


避免在類別的公開介面暴露成員資料。


對於從語義上破壞封裝性保持警惕。


察覺Demeter法則。


 


耦合性與抽象及封裝有著非常密切的關係。緊密的耦合性總是發生在抽象不嚴謹或封裝性遭到破壞的時候。


6.3 有關設計和實作的議題


包含(「有一個(has a)...」的關係)


包含才是物件導向程式設計中的主力技術。


經由包含來實作「有一個(has a)」的關係


在萬不得已時,經由private繼承來實作「有一個(has a)」的關係


作者建議不要使用這種作法。


警惕有超過約7個資料成員的類別


太多成員要考慮分解成更小的類別。


繼承(「是一個(is a)...」的關係)


繼承是某一個類別是另一個類別的一種特殊化。繼承能把共有元素集中在一個基底類別中,可避免在多處現重複的程式碼和資料。


使用繼承時,必須要做以下幾項決策:


對於每一個成員函式,它對衍生類別可見嗎?它應該有預設的實作嗎?這個預設的實作能被覆寫override嗎?


對於每一個資料成員,它應該對衍生類別可見嗎?


 


用public繼承來實作「是一個(is a)...」的關係


要使用繼承就要進行詳細說明,否則就不要用它。


繼承給程式增加了複雜度,它是一項危險的技術。


遵循Liskov替代原則


衍生類別必須能透過基底類別的介面而被使用,使用者無須瞭解兩者之間的差異。


如果遵守Liskov替代原則,繼承就能夠成為降低複雜度的強大工具。


確保只繼承需要繼承的部份


衍生類別可以只繼承成員函數的介面或實作,也可以介面與實作都繼承。


表6-1顯示子程式可以被實作和覆寫的幾種形式。繼承而來的子程式有三種基本狀況。


如果只想使用一個類別的實作而不是介面,應該使用包含而不是繼承。


不要覆寫一個不可覆寫的成員函式


把共用的介面、資料及操作放到繼承樹中盡可能高的位置


只有一個實例的類別是值得懷疑


單例模式除外。


只有一個衍生類別的基底類別也值得懷疑


有可能出現提前設計的情況。


衍生後覆寫了某個子程式,但在其中沒有做任合事,這種情況也值得懷疑


避免讓繼承體系過深


作者指出繼承有二到三層就很麻煩了。


儘量使用多型,避免大量的類型檢查


讓所有的資料都是private(而非 protected)


多重繼承


避免使用多重繼承。


為什麼有這麼多關於繼承的規則?


以下總結何時用繼承,何時用包含:


如果多個類別共享資料而非行為,應該是去建立這些類別可以包含的共用物件。


如果多個類別共享行為而非資料,應該是讓它們從共同的基底類別繼承而來,並在基底類別裡定義共用的子程式。


如果多個類別共享資料也共享行為,應該要讓它們從一個共同的基底類別繼承而來,並在基底類別裡定義共用的資料和子程式。


想由基底類別控制介面的時候用繼承,自己控制介面時使用包含。


 


成員函式和資料成員


讓類別中的子程式數量盡可能地少


用隱含的方式禁止你不需要的成員函式與運算子


善用private防止呼叫方程式存取成員函式和資料成員。


減少類別所呼叫的不同子程式的數量


對於其他類別的子程式的間接呼叫要盡可能地少


Demeter法則。


儘量減少類別和類別之間相互合作的範圍


實例化物件的種類數量。


在被實例化物件上直接呼叫的不同子程式的數量。


呼叫由其他物件回傳之物件的子程式的數量。


 


建構函數


 


應該在所有的建構函式中初始化所有的資料成員


用私用建構函式來強制實現單例屬性


 


看範例比較清楚,用私有建構函數完成單例。


優先採用深層副本,除非論證可行,才採用淺層副本


深層複本是物件成員資料逐項複製的結果。


淺層複本是指向或參考同一個物件。


兩者差別在於效能。


 


建立類別的原因


 


為現實世界中的物件建模


為抽象的物件建模


降低複雜度


隔離複雜度


隱藏實作細節


限制變動的影響範圍


隱藏全域資料


讓參數傳遞更順暢


建立中心控制點


讓程式碼更易於重用


為程式家族做計畫


把相關操作包裝到一起


實現某種特定的重構


 


應該避免的類別


避免建立萬能類別


避免權責過多的類別,明顯違反單一職責原則。


消除無關緊要的類


如果一個類別只有資料沒有行為,要考慮將類別取消。


避免用動詞命名類別


只有行為沒有資料也不是真正的類別,可考慮將類別改成其他類別的子程式。


總結:建立類別的理由


為現實世界中的物件建模


為抽象的物件建模


降低複雜度


隔離複雜度


隱藏實作細節


限制變動的影響範圍


隱藏全域資料


讓參數傳遞更順暢


建立中心控制點


讓程式碼更易於重用


為程式家族做計畫


把相關操作包裝到一起


實現某種特定的重構


 


6.5 與具體程式語言相關的問題


Java所有方法預設都可以覆寫,方法必須被定為final才能阻止衍生類別覆寫。


C++預設不可以覆寫方法,基底類別方法必須定義為virtual才能夠被覆寫。


VB基底類別必須定為overridable,衍生類別子程式必須要使用overrides關鍵字來進行覆寫。


類別相關不同語言有顯著差異的一些地方:


在繼承層次中被覆寫的建構函式和解構函式的行為。


在例外處理時建構函式和解構函式的行為。


預設建構函式(無參數建構函數)的重要性。


解構函式或終結器(finalizer)的呼叫時機。


和覆寫語言內建的運算子相關的知識(?)。


當物件被建立和銷燬時或被宣告或作用範圍結束時,處理記憶體的方式。


 


6.6 超越類別:套件 (Package)


 


statement 到 subroutine 到 class 到 package。


 


核對表:類別的品質


抽象資料類型(ADT)


 


是否將類別都看做是ADT?是否從這個角度評估它們的介面?


抽象


類別是否有一個中心目的?


類別名稱是否表達中心目的?


類別介面是否展現一致的抽象?


類別介面是否讓人清楚知道該如何使用?


類別介面是否抽象?能把類別看做是黑盒子嗎?


能讓其他類別無需動用其他資料?


是否刪除無關訊息?


是否盡可能將類別分解?


修改類別時是否維持介面的完整性?


 


封裝


 


是否把類別成員可存取性降到最低?


是否避免暴露類別中的資料成員?


是否隱藏實作的細節?


類別是否對使用者的行為進行假設?


類別是否不依賴於其他類別?它是鬆散耦合?


 


繼承


 


繼承是否是「是一個(is a)」的關係?衍生類別是否遵循LSP?


類別文件是否記錄繼承的策略?


衍生類別是否避免覆寫不可以覆寫的方法?


公用介面、資料和行為都放在高的繼承層次中?


繼承層次是否很淺?


基底類別所有成員資料是否都被定為private而非protected?


 


跟實作相關的其他問題


 


類別是否只有七個或更少的成員?


是否把類別呼叫其他類別子程式數量減到最少?


類別是否只在絕對必要時才與其他類別實作?


是否在建構函式中初始化所有資料成員?


除非特殊需要才使用淺層副本,類別是否都用深層副本設計?


 


與語言相關的問題


是否研究程式語言類別相關各種特有問題?


要點


類別的介面應提供一致的抽象。


類別的介面應隱藏一些資訊,例如系統介面、設計決策或實作細節。


包含比繼承優先考量,除非是「是一個(is a)」關係。


繼承會增加複雜度,有違軟體首要技術管理複雜度。


類別是管理複雜度的首選工具。


 



留言

這個網誌中的熱門文章

異世界NTR web版第三章 觀後感 喧賓奪主 ,反派實力過強

泛而不精的我被逐出了勇者隊伍 web第三章 觀後感 菲莉真能打; 露娜超爽der

持有縮小技能的D級冒險者,與聖女結婚並加入勇者團隊 漫畫 01-04 觀後感 大我與小我