本章主軸#

本章是一段「重新定義基本概念」的章節。作者退一步反思物件、封裝、繼承這些 OO 基礎詞彙,並提出一套從設計模式中提煉出來的新觀點。讀者學完本章,會以全新的方式看待物件導向,後續每個模式都會更好理解。

本章還會把設計模式與敏捷(agile)開發實踐連結起來。乍看兩者似乎對立,但作者指出兩者所追求的程式碼特質高度相關。

對「物件」的新觀點#

傳統觀點:資料 + 方法#

把物件視為「聰明的資料」——加幾個方法上去就成了物件。這只是從實作層次的觀察,太狹隘。

新觀點:擁有責任的東西#

從概念層次重新定義:

物件是擁有責任的實體,這些責任定義它的行為。

好處:

  • 開發可以分兩步走:先做不擔心細節的設計、再做實作
  • 透過公開介面與物件溝通,不必知道內部如何完成工作
  • 可以放心委派

把焦點從「實作」轉到「動機」是設計模式中的一貫主題——透過介面隱藏實作,可以與使用方完全解耦。

對「封裝」的新觀點#

傳統觀點:資料隱藏#

作者問學生「誰聽過封裝就是資料隱藏?」幾乎全部舉手。

作者的雨傘比喻#

「我有一把雨傘——它能容納幾個人、可以移動、有立體聲、能調溫、不用我自己推也能跑 ⋯⋯。其實大家叫它『汽車』,但對我而言它就是擋雨的工具。」

把汽車定義為雨傘,正如把封裝定義為資料隱藏——是一種限制思維的「定義陷阱」。

新觀點:任何形式的隱藏#

封裝可以隱藏的東西包括:

  • 資料
  • 實作(implementation)
  • 衍生類別(type encapsulation)
  • 設計細節
  • 實例化規則

Circle 包住 XXCircle,就是隱藏了實作;用抽象類別 Shape 對外,client 看不到 CircleSquare,就是隱藏了型別。GoF 提到「封裝」時通常指的就是這種型別封裝

對「繼承」的新觀點#

傳統用法:為了重用而特化#

例如先有 Pentagon,再衍生 PentagonSpecialBorder。看似乾淨,但問題是:

  • 凝聚變弱Pentagon 開始要管邊框、內部花紋等不相關的事
  • 難以重用:邊框邏輯被綁在 Pentagon 家族中,其他形狀拿不到
  • 不夠擴展:再加一個變動軸(例如陰影)就會類別爆炸

新用法:用繼承來「分類行為」#

繼承不是用來特化,而是用來把行為相同的事物收為同類。每個 ConcreteClass 是「相同概念」的不同實作。

找出變動點並封裝#

這是 GoF 的核心心法:

思考你的設計中哪些東西會變,並將會變的概念封裝起來。

要把這句話聽得進去,必須先接受「封裝是任何形式的隱藏」這個新觀點。實作上:

  • 用抽象類別或介面把「變動的概念」隱藏起來
  • 使用方持有這個抽象類別的參考
  • 透過抽象層分離雙方,達到鬆耦合

變動的兩種放置方式:資料 vs 行為#

動物例子#

需求:

  • 不同動物腳數不同(資料變動)
  • 不同動物移動方式不同(行為變動)

腳數很容易:放個資料成員就好。但是移動:

  • 用變數加 switch?多個變動軸交織後會很糟(飲食、地形、混合行為)
  • 用繼承?特化爆炸
  • 用內含物件Animal 內持有 AnimalMovement 物件 → 可獨立替換

Figure 8-3: Animal 持有 AnimalMovement 物件——以聚合容納行為變化

Figure 8-4: 把內含物件以資料成員的形式呈現

把行為包成物件,與把資料放成成員,本質上是同一件事——只不過內含的是「會變動的行為」而非「會變動的數值」。

在真正物件導向的語言裡,連數值(int、double)也都是物件,只是被特殊語法包裝。

共通與差異分析(CVA)#

來自 Jim Coplien 的方法:

  • Commonality analysis:找出家族成員的共通結構——「他們在哪裡相同」
  • Variability analysis:找出家族成員的變化方式——「他們在哪裡不同」

對映關係:

分析對應的 OO 元素對應的視角
共通分析抽象類別(abstract class)概念視角
規格設計介面(interface)規格視角
差異分析具象類別(concrete class)實作視角

Figure 8-5: 共通與差異分析、三種視角、抽象類別與其衍生類別之間的關係

用 CVA 找物件,比「在需求中找名詞」更可靠。它能避免設計出又高又脆的繼承樹。

設計過程化為兩步#

設計什麼應問自己
抽象類別(共通)要怎樣的介面才能涵蓋這個概念的所有責任?
衍生類別(差異)給定這個介面,這個變化要怎麼實作?

敏捷開發強調的程式碼特質#

設計模式與敏捷看似不同,但都共同追求:

  • 無冗餘(no redundancy)——「One rule, one place」原則
  • 可讀性(readability)——Ron Jeffries 的「program by intention」
  • 可測試性(testability)——Kent Beck 的測試先行(TDD)

這些特質與「強凝聚、鬆耦合、封裝」高度相關:

  • 凝聚 → 易測試
  • 解耦 → 易測試
  • 冗餘 → 測試成本變高
  • 可讀 → 測試一目了然
  • 封裝 → 測試對外無依賴

「Program by intention」——遇到要實作的功能,先當作它已經存在,給一個清楚命名的方法呼叫並繼續寫;之後再回頭實作這個方法。

這幾乎就是 GoF 的「Design to interfaces」翻成另一句話。

本章要記住的事#

  • 物件 = 「擁有責任的東西」,而非「資料 + 方法」
  • 封裝是任何形式的「隱藏」,不只資料
  • 繼承的最佳用途是「分類同類行為」,不是「特化以重用」
  • 把行為變動放進物件,與把資料變動放進成員,是同一個思路
  • 共通與差異分析是比「找名詞」更可靠的設計切入點
  • 設計模式與敏捷追求同一組程式碼特質