在計算的初期,程式設計師以「陳述句」思考程式;到了七八十年代,轉為以「常式」為單位思考;而在二十一世紀,**類別(Class)**成為了程式設計的核心。類別是一組資料與操作這些資料的常式之集合,共同承擔一項具有內聚性的職責。成為高效程式設計師的關鍵,在於最大化你可以安全忽略的程式部分,而類別正是達成這個目標的首要工具。
6.1 類別的基礎:抽象資料類型(ADTs)#
**抽象資料類型(Abstract Data Type, ADT)**是一組資料及其操作的集合。操作既對外描述了資料的意義,也提供了改變資料的途徑。理解 ADT 是理解物件導向程式設計的根本。
以控制字體為例,若不使用 ADT,你會散布 currentFont.size = 16 或 currentFont.attribute or 0x02 這類直接操作資料的程式碼。使用 ADT 的好處包括:
| 好處 | 說明 |
|---|---|
| 隱藏實作細節 | 資料型態改變時只需修改一處 |
| 介面更具資訊性 | 避免 16 究竟是像素還是點數的歧義 |
| 正確性更明顯 | 呼叫 SetBoldOn() 比 attribute or 0x02 容易驗證 |
| 自我文件化 | 常式呼叫比位元運算更具可讀性 |
| 無須到處傳遞資料 | ADT 的結構自行持有資料 |
| 以真實世界實體操作 | 能以真實世界實體而非底層結構進行操作 |
設計 ADT 時,用問題領域的語言思考。若堆疊代表一組員工,就以「員工」操作它。命名應獨立於儲存媒介——用
rateTable.Read()而非rateFile.Read()。即使是只有開和關的燈,封裝後也更易讀、易改。
ADT 構成了類別概念的基礎。類別可以被理解為 ADT 加上繼承(Inheritance)和多型(Polymorphism)。
6.2 良好的類別介面#
建立高品質類別最重要的第一步,就是設計一個好的介面——為介面建立好的抽象,並確保細節被隱藏在抽象之後。
好的抽象#
| 準則 | 說明 |
|---|---|
| 呈現一致的抽象層級 | 每個類別應實作一個且僅一個 ADT。若 EmployeeCensus 同時暴露「員工」和「清單容器」的操作,就是混合了兩種 ADT |
| 確認要實作哪一種抽象 | 只需格線控制項的 15 個常式時,不要暴露試算表控制項的全部 150 個 |
| 成對提供互補操作 | 有 TurnLightOn() 就應有 TurnLightOff() |
| 將無關資訊移到其他類別 | 若一半常式操作一半資料,其實是兩個類別偽裝成一個 |
| 盡量以程式化方式約束介面 | 編譯器能強制的約束比文件描述更可靠 |
| 維護介面抽象的完整性 | 不要添加不一致的成員,如在 Employee 中加入 IsZipCodeValid() 或 SQL 方法 |
同時考慮抽象與內聚性:呈現良好抽象的介面通常有強內聚性。當內聚性弱時,反問「這個類別是否呈現了一致的抽象?」
好的封裝#
**封裝(Encapsulation)**比抽象更強——抽象讓你忽略實作細節,封裝則阻止你去看。
封裝的核心準則
- 最小化可存取性:偏好最嚴格的存取層級(private > protected > public)
- 不要將成員資料設為 public:使用 getter/setter 維持封裝
- 避免暴露私有實作細節:C++ 可用 Pimpl 慣用法隱藏 private 區段
- 不要對類別使用者做假設、避免友元類別
- 偏好讀取時的便利性勝過撰寫時的便利性
語意層面的封裝破壞比語法層面更難防範。例如因為「知道」
Retrieve()會自動連線而不呼叫Connect(),就是透過實作而非介面在程式設計。正確做法是改善介面文件,而非翻看原始碼。
6.3 有關設計和實作的議題#
包含(Containment,「has a」關係)#
包含是物件導向程式設計中的主力技術。透過將物件作為成員資料來實作「has a」關係。只有萬不得已時才透過 private 繼承實作包含。類別的資料成員數以 7 個左右為上限(7±2 原則)。
繼承(Inheritance,「is a」關係)#
繼承旨在透過基底類別定義共用元素來簡化程式碼,但也增加複雜度。
繼承的設計準則
- 以 public 繼承實作「is a」:不遵守基底類別介面契約就不該繼承
- 設計並記錄可繼承性,否則就禁止繼承(C++ 用 non-virtual、Java 用 final)
- 遵守 Liskov 替換原則(LSP):子類別必須能透過基底類別介面使用,使用者不需知道差異
- 只繼承你需要的部分:常式分三種——抽象可覆寫、可覆寫(有預設實作)、不可覆寫
- 不要「覆寫」不可覆寫的成員函式、將共用元素盡量往繼承樹上層移動
- 對只有一個實例、只有一個衍生類別、或覆寫後什麼都不做的類別保持懷疑
- 避免過深的繼承樹(二到三層是實務上限);偏好多型而非大量 type checking
- 所有資料成員都應設為 private,不要用 protected:若衍生類別需存取,請提供 protected 的存取函式
- **多重繼承(Multiple Inheritance)**主要適用於 mixin——如
Displayable、Serializable等簡單的屬性混入類別。應謹慎評估其對系統複雜度的影響
繼承的規則都指向一個核心訊息:繼承傾向於對抗管理複雜度的首要技術使命。共享資料但非行為時用包含;共享行為但非資料時用繼承;兩者皆有則繼承。想控制介面時用包含,想讓基底類別控制介面時用繼承。
flowchart TD
A["需要共享什麼?"] --> B{"共享資料但非行為?"}
B -- 是 --> C["使用包含"]
B -- 否 --> D{"共享行為但非資料?"}
D -- 是 --> E["使用繼承"]
D -- 否 --> F{"兩者皆有?"}
F -- 是 --> G["使用繼承"]
F -- 否 --> H{"想控制介面?"}
H -- 是 --> I["使用包含"]
H -- 否 --> J{"想讓基底類別控制介面?"}
J -- 是 --> K["使用繼承"]成員函式與資料#
- 保持常式數量盡可能少,禁止不需要的隱式生成之運算子
- 最小化 fan out:減少類別呼叫的不同常式總數
- 遵守迪米特法則(Law of Demeter):
account.ContactPerson()可以,但account.ContactPerson().DaytimeContactInfo().PhoneNumber()不行
建構子#
- 在所有建構子中初始化所有成員資料
- 使用 private 建構子強制實施 Singleton 模式
- 偏好深層複製(Deep Copy),除非有驗證過的效能理由才用淺層複製
6.4 建立類別的原因#
建立類別的理由遠不止於模擬真實世界物件:
| 理由 | 說明 |
|---|---|
| 模擬真實世界或抽象物件 | 如 Shape 是 Circle、Square 的抽象 |
| 降低複雜度 | 建立類別的最重要理由——隱藏資訊使你無需再想它 |
| 隔離複雜度 | 錯誤更易定位與修復 |
| 隱藏實作細節 | 限制變更影響範圍 |
| 隱藏全域資料 | 透過存取常式包裝 |
| 精簡參數傳遞 | 頻繁傳遞同一參數暗示應納入同一類別 |
| 建立集中控制點 | 促進程式碼重用(NASA 研究顯示 OO 專案可重用超過 70% 程式碼) |
| 為程式家族做規劃 | 包裝相關操作、完成特定重構 |
應避免的類別#
- 上帝類別(God Classes):不斷透過
Get()/Set()操作他人資料的類別,功能應歸屬於被操作的類別 - 無關緊要的類別:只有資料沒行為,考慮降級為其他類別的屬性
- 以動詞命名的類別:只有行為沒資料(如
DatabaseInitialization),應成為其他類別的常式
6.5 與具體程式語言相關的問題#
不同語言對類別的處理方式差異顯著。例如:Java 中常式預設可覆寫(需用 final 防止);C++ 預設不可覆寫(需用 virtual 允許)。其他差異領域包括繼承樹中建構子/解構子行為、例外處理條件下的行為、預設建構子的重要性、覆寫內建運算子的適當性,以及物件建立與銷毀時的記憶體處理。
6.6 超越類別:套件(Package)#
類別是目前達成模組化的最佳方式,但模組化是個更大的課題。軟體開發的進步很大程度來自於不斷提高聚合的粒度——從陳述句、到副程式、到類別。
若程式語言不直接支援套件,可透過命名慣例區分公開與私有類別、以專案結構標示所屬套件、以規則定義套件間的使用關係。
更多資源#
- Meyer, Bertrand. Object-Oriented Software Construction, 2d ed., 1997 – ADT 基礎與繼承深入討論
- Riel, Arthur J. Object-Oriented Design Heuristics, 1996 – 類別層級設計的實用建議
- Meyers, Scott. Effective C++ / More Effective C++ – C++ 經典參考
- Bloch, Joshua. Effective Java Programming Language Guide, 2001 – Java 與物件導向實踐
要點#
- 類別介面應提供一致的抽象,許多問題都源自於違反這項原則。
- 類別介面應隱藏某些東西——系統介面、設計決策或實作細節。
- 除非是模擬「is a」關係,否則包含通常優於繼承。
- 繼承是有用的工具,但它增加複雜度,這與軟體管理複雜度的首要技術使命相悖。
- 類別是你管理複雜度的主要工具,應給予其設計足夠的關注。