Chapter 6 Working Classes (Page 162 - 197)
6.1 Class Foundations: Abstract Data Types(ADTs)
類的基礎 : 抽象資料類型(ADTs)
ADT是指一些資料以及對這些資料所進行的操作的集合。
6.2 ADTs and Classes
Good Class Interfaces
把每個抽象資料類型用他自己的類實現。
類還涉及到繼承和多態兩個額外的概念。因此,考慮類的另一種方式就是把它看做是抽象資料類型再加上繼承和多態兩個概念。
同時考慮抽象性和內聚性
一個呈現出很好的抽象的類介面通常也有很高的內聚性。而具有很強內聚性的類往往也會呈現為很好的抽象。
良好的封裝 Good Encapsulation
封裝是一個比抽象更強的概念。抽象通過提供一個可以讓你忽略實現細節的模型來管理複雜度,而封裝則強制阻止你看到細節。
封裝的原則:
1. 儘可能的限制類和成員的可訪問性(accessibility)
2. 避免把私用的實現細節放入類的介面中。
3. 留意過於緊密的耦合關係(coupling)
a. 儘可能的限制類和成員的可訪問性。
b. 避免友元類,因為他們之間是緊密耦合的。
c. 在類中把資料聲明成Private而不是Protected,以降低生類和基類之間的耦合的程度。
d. 避免在類的公開介面中暴露成員資料。
e. 要對從語義上破壞封裝性保持警惕。
f. 察覺 Demeter法則
6.3 有關設計和實現的問題
Design and implementation Issues
1. 包含(“有一個……”的關係) Containment
警惕有超過7個資料成員的類
研究表明,人們在做其他事情時,能夠記住的離散項目的個數
是7+-2個。如果一個類中包含有超過7個資料成員時,請考慮要不要把他分解成為幾個更小的類。
2. 繼承(“是一個……”的關係)Inheritance
繼承的概念是說一個類是另一個類的特化(specialization)。
目的,繼承能夠把共有的元素集中在一個基類中,從而有助於避免在多處出現重複的代碼和資料。
a. 要麼使用繼承並進行詳細說明,要麼就不要使用它。因為繼承給程式增加了複雜度,因此它是一種危險的技術。
b. 遵循Liskov替換原則
除非衍生類別真的“是一個”更特殊的基類,否則不應該從基類繼承。
總結為:衍生類別必須能通過基類的介面而被使用,而且使用者無須瞭解兩者之間的差易。
c. 確保只繼承需要繼承的部分
d. 不要“覆蓋”一個不可覆蓋的成員函數。
C++和JAVA兩種語言都允許“覆蓋”那些不可覆蓋的成員函數。例如:
一個成員函數在基類中是私人(Private)的話, 其衍生類別可以建立一個同名的成員函數。
這個函數是令人困惑的,因為它看上去似乎應該是多態的,但是事實上並非如此,只是同名而已。
e. 把共用的介面,資料及操作放到繼承樹中儘可能高的位置。
f. 只有一個執行個體的類是值得懷疑的
只需要一個執行個體,這可能表明設計中把對象和類混為一談了。
g. 只有一個衍生類別的基類也是值得懷疑的。
每當我看到只有一個衍生類別的基類時,我就懷疑某個程式員又在進行“提前設計”了----也就是試圖去預測未來的需要,而又常常沒有真正瞭解未來到底需要什麼。 也就是說,不要建立任何並非絕對必要的繼承結構。
h. 派生後覆蓋了某個子程式,但又在其中沒做任何的操作,這種情況也值得懷疑。這通常表明基類的設計有錯誤。
i. 避免讓繼承體系過深。
物件導向的編程方法提供了大量可以用來管理複雜度的技術。然而每種強大的工具都有其危險之處,甚至有些物件導向技術還有增加發雜度的趨勢。
建議把繼承層次限制在6層之內。
j. 盡量使用多態,避免大量的類型檢查。
k. 讓所有的資料都是Private(而非Protected)
“繼承會破壞封裝。”
多重繼承 Multiple Inheritance
雖然有些專家建議廣泛使用多重繼承,但是,多重繼承的用途主要是定義
“混合體(Mixins)”,也就是一些能給對象增加一組屬性的簡單類。
混合體需要使用多重繼承,但只要所有的混合體之間保持完全獨立,他們不會導致典型的菱形繼承(diamond-inheritance)問題。
JAVA和VB語言是認可混合體的價值,因為他們允許多重介面繼承,但只能繼承一個類的實現。
而C++則同時支援介面和實現的多重繼承。
程式員在決定使用多重繼承之前,應該仔細地考慮其他替代方案,並謹慎地評估它可能對系統的複雜度和可理解性產生的影響。
Why Are There So Many Rules for Inheritance?
為什麼有這麼多關於繼承的規則?
總結一下什麼時候可以使用繼承,何時又該使用包含:
1. 如果多個類共用資料而非行為,應該建立這些類可以包含的共用對象。
2. 如果多個類共用行為而非資料,應該讓他們從共同的基類繼承而來,並在基類裡定義共用的子程式。
3. 如果多個類機共用資料也共用行為,應該讓他們從一個共同的基類繼承而來,並在基類裡定義共用的資料和子程式。
4. 當你想由基類控制介面時,使用繼承;當你想自己控制介面時,使用包含。
Member Function and Data
成員函數和資料成員
有效地實現成員函數和資料成員的建議:
1. 讓類中子程式的數量儘可能少
2. 禁止隱式地產生你不需要的成員函數和運算子
3. 減少類所調用的不同子程式的數量
4. 對其他子程式的間接調用要儘可能減少
5. 一般來說,應盡量減少類和類之間相互合作的範圍
盡量減少:a. 所執行個體化的對象的種類。 b. 在被執行個體化對象上直接調用 不同子程式的數量。c. 調用由其他對象返回的對象的子程式的數量。
Constructors
建構函式
1. 如果可能,應該在所有的建構函式中初始化所有的資料成員。
在所有的建構函式中初始化所有的資料成員是一個不難做到的防禦式編程實踐。
2. 用Private建構函式來強制實現單件屬性。(singleton property)
如果想定義一個類,並需要強制規定它只能有唯一一個執行個體對象的話,可以把該類所有的建構函式都隱藏起來,然後對外提供一個static的GetInstance()
子程式來訪問該類的唯一執行個體。列如:以下Java code:
public class MaxId
{
//constructors and destructors
private MaxId() {...}
//public routines
public static MaxId GetInstance()
{
return m_instance;
}
//private members
private static final MaxId m_instance = new MaxId();
...
}
3. 優先採用深層複本(Deep Copies);除非論證可行,才採用淺層複本
(shallow copies)
Reason to Create a Class
建立類的理由
下面列出一些建立類的合理原因:
1. 為現實世界中的對象建模
2. 為抽象的對象建模。
3. 降低複雜度
4. 隔離複雜度
5. 隱藏實現細節
6. 限制變動的影響範圍
7. 隱藏全域資料
8. 讓參數傳遞更加順暢。
9. 建立中心控制點
10. 讓代碼更易於重用。
11. 為程式族做計劃
12. 把相關的操作封裝到一起
13. 實現某種特定的重構
Classes to Avoid
應該避免的類
下面就是一些應該避免建立的類:
1. 避免建立萬能類(God class)
2. 消除無關緊要的類
3. 避免用動詞命名的類
只有行為而沒有資料的類往往不是一個真正的類。
請考慮把DatabaseInitialization或StringBuilder這樣的類 變成其他類的子程式。
6.5 Language-Specific Issue
與具體程式設計語言相關的問題
例如: 如何在一個衍生類別中通過覆蓋成員函數來實現多態。
在JAVA中,所有的方法預設可以被覆蓋,除非加上Final。
在C++中,所有的方法預設不可以被覆蓋,除非加上Virtual。
6.6 Beyond Classes: Packages
超越類:包
類是當前程式員們實現模組化(modularity)的最佳方式。
要點總結:Key Point
1. 類的介面應該提供一致的抽象。很多問題都是由於違背該原則而引起的。
2. 類的介面應隱藏一些資訊----如某個系統介面,某項設計決策,或一些實現細節。
3. 包含往往比繼承更為可取----除非你要對“是一個/is a”的關係建模。
4. 繼承是一種有用的工具,但他卻會增加複雜度,這有違背軟體的首要技術使命
----管理複雜度。
5. 類是管理複雜度的首選工具。要在設計類時給予足夠的關注,才能實現這一目標。