類別(Class)的概念遠在柏拉圖之前就已存在。柏拉圖的理型(Platonic solids)就是類別,它們在現實世界中有各種實例。柏拉圖的「理想球體」是絕對完美但不可觸及的,而我們身邊能觸摸到的球體,卻都有某種不完美。
物件導向程式設計沿襲了這個哲學傳統,將程式分為:
- 類別(Class):對一整組相似事物的通用描述
- 物件(Object):具體的事物本身
類別在溝通上非常重要,因為它們可以描述大量具體事物。類別層級的模式在所有實作模式中影響範圍最廣;相較之下,設計模式(Design Patterns)通常談的是類別之間的關係。
本章包含以下模式:
| 模式 | 說明 |
|---|---|
| Class | 用類別表達「這些資料屬於一起,這些邏輯與它們相關」 |
| Simple Superclass Name | 用簡短的名稱為類別階層的根命名,名稱應來自同一隱喻 |
| Qualified Subclass Name | 子類別名稱應傳達與父類別的相似之處與差異 |
| Abstract Interface | 將介面與實作分離 |
| Interface | 用 Java interface 定義不常變動的抽象介面 |
| Versioned Interface | 透過引入新的子介面來安全地擴充介面 |
| Abstract Class | 用抽象類別定義可能會變動的抽象介面 |
| Value Object | 撰寫行為如同數學值的物件 |
| Specialization | 清楚表達相關運算之間的相似與差異 |
| Subclass | 用子類別表達單一維度的變化 |
| Implementor | 覆寫方法以表達運算的變體 |
| Inner Class | 將局部使用的程式碼封裝在私有類別中 |
| Instance-specific Behavior | 依實例改變邏輯 |
| Conditional | 用明確的條件判斷改變邏輯 |
| Delegation | 將邏輯委派給不同類型的物件 |
| Pluggable Selector | 透過反射(Reflection)執行方法來改變邏輯 |
| Anonymous Inner Class | 在建立新物件的方法中直接覆寫一兩個方法 |
| Library Class | 將不屬於任何物件的功能以靜態方法集合呈現 |
Class#
資料的變動比邏輯更頻繁——這是使類別發揮作用的關鍵觀察。每個類別都是一種宣告:
「這些邏輯屬於同一群組,而且比它們所操作的資料值變化得更慢。這些資料值也屬於同一群組,以相近的速率變化,並由相關的邏輯操作。」
這種「會變的資料」與「不變的邏輯」之間的嚴格分離並非絕對:
- 有時邏輯會因資料值而稍有不同
- 有時邏輯差異很大
- 有時資料在運算過程中不會改變
學會如何將邏輯封裝在類別中,並表達邏輯的變化,是有效使用物件程式設計的重要一環。
將類別組織成**階層(Hierarchy)**是一種壓縮形式——將父類別的內容隱含地包含在所有子類別中。但如同所有壓縮技巧,這會讓程式碼更難閱讀:你必須先理解父類別的脈絡,才能理解子類別。
謹慎使用繼承是物件程式設計的另一個重要面向。建立子類別等於在說:「我跟那個父類別一樣,只是有些不同。」
類別是程式中相對昂貴的設計元素。每個類別都應該做有意義的事。減少系統中的類別數量是一種改善——前提是剩下的類別不會因此變得臃腫。
Simple Superclass Name#
找到恰如其分的名稱,是程式設計中最令人滿足的時刻之一。你正在與一個概念搏鬥,程式碼變得複雜卻似乎不需要那麼複雜。然後,常常是在對話中,有人說:「喔!這其實就是一個 Scheduler。」所有人都鬆了一口氣——正確的名稱會引發一連串進一步的簡化與改善。
類別名稱是設計中最核心的錨點概念,最重要的名稱選擇莫過於類別名稱。一旦類別有了好名稱,操作的名稱自然隨之而來。反過來的情況很少見,除非一開始的類別就命名不當。
簡潔與表達力的張力#
在命名類別時,簡潔與表達力之間存在張力:
- 你會在對話中使用類別名稱:「你有記得先旋轉(rotate)Figure 再平移(translate)嗎?」
- 名稱應該簡短有力
- 但精確的名稱有時似乎需要好幾個字
善用隱喻#
化解這個兩難的方法是為運算挑選一個強而有力的隱喻(Metaphor)。有了隱喻,即使單一個字也能帶來豐富的聯想、連結與暗示。
例如,在 HotDraw 繪圖框架中:
- 最初的名稱是
DrawingObject - Ward Cunningham 引入了排版隱喻:繪圖就像已排版的頁面,頁面上的圖形元素就是 figures
- 因此類別改名為
Figure——在這個隱喻脈絡下,Figure比DrawingObject更簡短、更豐富、也更精確
命名需要時間#
- 好名稱有時需要時間去發現
- 程式碼可能已經「完成」且運作了數週、數月,甚至數年後,你才發現更好的類別名稱
- 工具包括:查閱同義詞辭典、列出最不合適的名稱、散步、與人對話
為重要的類別尋找單字名稱。對話是持續幫助找到更好名稱的工具——向他人解釋物件的用途,會引導你尋找生動而有表現力的意象。
Qualified Subclass Name#
子類別的名稱有兩個職責:
- 傳達它像哪個類別
- 傳達它有何不同
與階層根部的名稱不同,子類別名稱在對話中不常使用,因此可以犧牲簡潔來換取表達力。做法是在父類別名稱前加上一個或多個修飾詞來形成子類別名稱。
例外情況#
當子類別化僅作為實作共享機制,而子類別本身是一個重要概念時,應賦予它自己的簡單名稱。
例如,HotDraw 有一個 Handle 類別,呈現被選取圖形的編輯操作:
- 它被簡單地稱為
Handle,儘管它繼承自Figure - 有一整個 Handle 家族,適當地命名為
StretchyHandle、TransparencyHandle等 - 因為
Handle是自己階層的根,它更需要一個 Simple Superclass Name 而非 Qualified Subclass Name
flowchart TD
A["需要為子類別命名"] --> B{"子類別是否為\n新概念階層的根?"}
B -->|"是"| C["使用 Simple Superclass Name\n例:Handle"]
B -->|"否"| D{"使用頻率高?\n在對話中常提到?"}
D -->|"是"| E["偏好簡短名稱"]
D -->|"否"| F["使用 Qualified Subclass Name\n父類別名稱 + 修飾詞\n例:StretchyHandle"]多層階層的命名#
多層階層通常是委派(Delegation)等著發生的訊號,但在它們存在期間仍需要好名稱:
- 不要盲目地在直接父類別前加修飾詞
- 從讀者的角度思考:讀者需要知道這個類別像哪個類別?
- 以那個父類別作為子類別名稱的基礎
與人溝通是類別名稱的目的。對電腦來說,類別可以直接用數字編號。太長的類別名稱難以閱讀和排版;太短的名稱則增加讀者的短期記憶負擔。名稱彼此無關的類別群組難以理解和記憶。善用類別名稱來述說你的程式碼的故事。
Abstract Interface#
軟體開發中的古老格言是:針對介面寫程式,而非針對實作。這是另一種說法——設計決策不應該在超過必要的範圍內被看見。如果大部分程式碼只知道它在處理一個 Collection,你就可以自由地在日後更換具體類別。然而,在某個時刻你確實需要指定一個具體類別,電腦才能執行運算。
這裡所說的「介面」指的是「一組沒有實作的操作」。在 Java 中可以用 interface 或父類別(superclass)來表示。
介面的成本#
每一層介面都有成本——它是需要學習、理解、撰寫文件、除錯、組織、瀏覽和命名的額外項目。最大化介面數量並不會最小化軟體成本。只在確實需要介面帶來的彈性時才付出這個代價。
由於你通常無法預知何處需要彈性,最小化成本的做法是:結合對介面引入點的推測,與在實際需要彈性時才加入介面。
軟體的不可預測性#
儘管我們抱怨軟體缺乏彈性,但對於任何給定系統,有非常多方面我們根本不需要它具備彈性。從基本層面(整數有幾個位元)到大規模變更(新的商業模式),大多數軟體不需要在大多數可能的方面保持彈性。
軟體變更的原因不僅僅是需求收集做得不好或贊助者改變主意——還有合理的變更。就像天氣以不可預測的方式變化,需求和技術也以不可預測的方式變化。這並不免除我們盡全力開發客戶現在需要的系統的責任,但暗示了透過推測來「未來防護」軟體的價值是有限的。
綜合考量彈性的需求、成本與不可預測性,引入彈性的最佳時機是確實需要的時候。引入彈性會因為需要修改現有軟體而產生成本;如果你無法親自修改所有需要修改的軟體,成本會進一步上升。
Java 的兩種抽象介面機制——父類別與介面——對於此類變更有不同的成本特性。
Interface#
在 Java 中宣告 interface 是表達「這是我想完成的事,其他都是不需要關心的細節」的一種方式。
Interface 是 Java 首次帶入大眾市場語言的重要創新之一,它在彈性與複雜度之間取得了良好平衡:
- 擁有多重繼承的部分彈性,但沒有其複雜性和歧義
- 一個類別可以宣告自己參與多個 interface
- Interface 只揭露操作,不揭露欄位,因此能有效保護使用者免受實作變更的影響
Interface 的限制#
- Interface 讓實作的變更變得容易,但不利於介面本身的變更
- 對 interface 的任何新增或修改都需要修改所有實作者
- 如果無法修改這些實作,大量使用 interface 會對後續設計演進產生顯著的阻力
- 所有操作都必須是
public——無法使用 package-visible 的操作
命名風格#
兩種命名方式取決於你如何看待 interface:
Interface 視為沒有實作的類別:以類別的方式命名(Simple Superclass Name、Qualified Subclass Name)
- 問題:好名稱被 interface 用掉了,實作類別只能叫
ActualFile、ConcreteFile或FileImpl - 優點:溝通上是在處理具體或抽象物件比較重要,而它是 interface 還是 superclass 比較不重要;這種命名支援日後在兩者間切換
- 問題:好名稱被 interface 用掉了,實作類別只能叫
強調 interface 的身份:在名稱前加上
I前綴- 如果 interface 叫
IFile,類別可以簡單地叫File
- 如果 interface 叫
Abstract Class#
在 Java 中表達抽象介面與具體實作之間區別的另一種方式是使用父類別(Superclass)。此處的「抽象」是指它可以在執行期被任何子類別替換,無論它在 Java 語義上是否為 abstract。
Abstract Class vs. Interface 的取捨#
關鍵在兩個議題:
介面本身的變更:Java interface 不善於支援介面結構的變更——每次修改都需要改動所有實作者,廣泛實作的 interface 容易導致設計癱瘓。Abstract class 則沒有此限制:只要能指定預設實作,就可以新增操作而不影響現有實作者。
多重介面的需求:Abstract class 的限制是實作者只能宣告效忠一個父類別。如果需要同一類別的其他視角,必須透過 Java interface 實現。
實務建議#
- 使用
abstract關鍵字告訴讀者:要使用這個類別就必須做一些實作工作 - 如果有任何機會讓階層的根類別本身有用且可實例化,就這樣做
- 一旦走上抽象之路,很容易走得太遠——努力讓根類別可實例化能鼓勵你消除不太可能產生價值的抽象
Interface 和類別階層並不互斥。你可以提供一個 interface 說「這是存取此功能的方式」,同時提供一個 superclass 說「這是實現此功能的一種方式」。在這種情況下,變數應宣告為 interface 型別,讓未來的維護者可以自由替換新的實作。
Versioned Interface#
當你需要變更一個 interface 卻無法直接修改時該怎麼辦?典型情況是想要新增操作。由於新增操作會破壞所有現有實作者,你不能直接這樣做。
解決方案:宣告一個新的 interface 來繼承原始 interface,並在新 interface 中新增操作。
- 需要新功能的使用者使用擴充的 interface
- 現有使用者對新 interface 的存在毫不知情
- 存取新操作時必須明確檢查型別並向下轉型(downcast)
範例#
考慮一個簡單的命令介面:
interface Command {
void run();
}一旦這個 interface 被發布並被擴充了上千次,修改它就變得非常昂貴。但為了支援命令的復原(undo),你需要新操作。Versioned Interface 的解決方式:
interface ReversibleCommand extends Command {
void undo();
}現有的 Command 實例照常運作。ReversibleCommand 的實例可以在任何需要 Command 的地方使用。要使用新操作時,向下轉型:
Command recent = ...;
if (recent instanceof ReversibleCommand) {
ReversibleCommand downcasted = (ReversibleCommand) recent;
downcasted.undo();
}注意事項#
- 使用
instanceof通常會降低彈性,因為它將程式碼綁定到特定類別 - 但在此情境下是合理的,因為它使 interface 的演進成為可能
- 如果出現多個替代 interface,客戶端需要做大量工作處理所有變化——這是重新思考設計的訊號
替代 interface 是一個醜陋問題的醜陋解法。Interface 不像容納實作變更那樣容易容納結構變更。替代 interface 建立了一種類似 Java 但有新規則的新「語言」——撰寫新語言的遊戲規則比撰寫應用程式嚴格得多。但如果你已經陷入需要擴充 interface 的處境,知道這個做法是有用的。
Value Object#
物件帶有可變狀態是思考運算的一種有價值的方式,但不是唯一的方式。數學經過數千年的發展,提供了一種在絕對真理與確定性的抽象世界中思考的方法。
Java 中的所謂基本型別(大多)屬於數學世界:
- 在 Java 中對一個數字加 1 是一種數學陳述
- 你不會改變一個變數的值來加 1——你建立一個新值
- 無法改變
0本身,但對大多數物件可以
函數式 vs. 狀態式#
- 函數式風格:永遠不改變任何狀態,只建立新值。適合(也許短暫地)靜態的情境
- 狀態式風格:情境隨時間變化時,狀態是適當的
某些情境可以用任一方式思考。例如,畫圖可以表示為:
- 對某個圖形媒介(如點陣圖)的狀態變更
- 用靜態描述來描述同一張圖

Figure 5.1: Graphics represented as procedures and objects
哪種表示最有用取決於個人偏好,也取決於圖片的複雜度和變更頻率。
程序式介面的問題#
- 程序呼叫的順序成為介面含義的重要(但常是隱含的)部分
- 修改這樣的程式很棘手——看似微小的變更可能因為隱含的順序語義改變而產生意外後果
數學表示的美#
- 順序幾乎不重要
- 你建立了一個可以做出絕對、無時間限制陳述的世界
盡可能建立數學的微型世界,並從帶有可變狀態的物件來管理它們。
範例:會計系統#
以不可變的數學值實作基本交易:

Figure 5.2: State-changing objects referring to immutable objects at the edges
class Transaction {
int value;
Transaction(int value, Account credit, Account debit) {
this.value = value;
credit.addCredit(this);
debit.addDebit(this);
}
int getValue() {
return value;
}
}Transaction 一旦建立就無法改變任何值。建構子還表明所有交易都會記入兩個帳戶。讀這段程式碼時,你知道不需要擔心交易四處漂浮或交易值在記帳後被更改。
實作 Value-style 物件#
- 劃定狀態世界與值世界的界線(例如
Transaction是值,Account持有可變狀態) - 在建構子中設定所有狀態,物件中其他地方不再有欄位指派
- 操作總是回傳新物件,由請求者儲存
bounds.translateBy(10, 20); // 可變的 Rectangle
bounds = bounds.translateBy(10, 20); // value-style Rectangle反對意見與限制#
- 最大的反對意見是效能——需要建立大量中間物件可能對記憶體管理造成壓力
- 但在整體程式設計成本中,這個論點通常站不住腳,因為大多數程式段落不是效能瓶頸
- 其他原因:對此風格不熟悉、難以劃定狀態變更與不可變的界線
- 部分 value-style 的物件是最糟糕的情況:介面較複雜,但又不能安全假設狀態不會改變
有時你的程式最佳的表達方式是狀態變更物件與代表數學值的物件的組合。物件、函式、程序三種主要風格如何有效融合,是一個值得深入探索的課題。
Specialization#
清楚傳達運算之間的相似與差異,能讓程式更容易閱讀、使用和修改。實際上,每個程式並非獨一無二——許多程式表達相似的概念,同一程式的不同部分也經常表達相似的概念。
變化的光譜:
- 最簡單的變化:只有狀態不同。字串
"abc"與"def"不同,但操作它們的演算法完全相同(例如計算長度) - 最複雜的變化:邏輯完全不同。符號積分程式與數學排版程式沒有共同邏輯,即使輸入可能完全相同
- 中間地帶:資料大部分相同但略有不同;邏輯大部分相同但略有不同
邏輯與資料的界線是模糊的。旗標(flag)是布林資料但影響控制流程;輔助物件可以存在欄位中但用於影響運算。
有效表達邏輯中的相似與差異,會為程式碼的進一步擴展開啟新的機會。
Subclass#
宣告子類別(Subclass)是一種說法:「這些物件跟那些一樣,除了……」。有了正確的父類別,建立子類別是一種強大的程式設計方式——用正確的方法覆寫,你可以用幾行程式碼引入現有運算的變體。
子類別化的歷史與限制#
當物件導向剛流行時,子類別化似乎是萬靈丹。最初用於分類——Train 是 Vehicle 的子類別,不管它們是否共享任何實作。後來人們認識到,既然繼承所做的是共享實作,它最有效的用途是提取公共實作。
但子類別化的限制很快就顯現了:
- 只能使用一次:如果發現某組變化不適合用子類別表達,要解開程式碼再重構需要不少工作
- 理解的負擔:必須先理解父類別才能理解子類別,隨著父類別變複雜,這個限制越來越大
- 父類別變更的風險:子類別可能依賴父類別實作的微妙屬性
- 深層繼承階層加劇以上所有問題
平行階層的問題#
特別有害的繼承使用方式是建立平行階層(Parallel Hierarchies)——這個階層的每個子類別都需要那個階層的對應子類別。這是一種重複,建立了類別階層之間的隱含耦合。
classDiagram
class Contract {
+product
}
class InsuranceContract {
+insuranceProduct
}
class PensionContract {
+pensionProduct
}
class Product
class InsuranceProduct
class PensionProduct
Contract <|-- InsuranceContract
Contract <|-- PensionContract
Product <|-- InsuranceProduct
Product <|-- PensionProduct
InsuranceContract ..> InsuranceProduct : 耦合
PensionContract ..> PensionProduct : 耦合
Figure 5.3: Parallel hierarchies
例如一個保險系統:InsuranceContract 無法引用 PensionProduct,將 product 欄位下移到子類別也不理想。解決方案(花了近一年才接近完成)是重新安排變化,使 Contract 無論用於保險或退休金都以相同方式運作,這需要建立一個新物件來表示預期現金流。

Figure 5.4: Hierarchy with duplication eliminated
有效使用子類別的關鍵#
有效的子類別關鍵在於徹底分解父類別的邏輯——將邏輯拆成各做一件事的方法。撰寫子類別時,你應該只需要覆寫恰好一個方法。如果父類別的方法太大,你就不得不複製程式碼再編輯。

Figure 5.5: Code copied and modified in a subclass
複製的程式碼在兩個類別之間引入了醜陋的隱含耦合——你無法安全地修改父類別中的程式碼,而不檢查並可能修改所有被複製的地方。
設計目標#
目標是能夠隨意在策略之間切換,視程式碼目前的需求而定:
- 用條件判斷(Conditional)
- 用子類別(Subclass)
- 用委派(Delegation)
視覺化這三種表達方式,看看不同策略是否有優勢,朝那個方向嘗試幾步看是否改善了程式碼。
flowchart TD
A["需要表達\n實例特定行為"] --> B{"邏輯是否會\n在運行時改變?"}
B -->|"不會"| C{"變化的維度\n有多少?"}
B -->|"會"| D["Delegation\n委派給可替換的物件"]
C -->|"單一維度"| E["Subclass\n用繼承表達變化"]
C -->|"簡單/少量"| F["Conditional\nif/switch 條件判斷"]
style D fill:#f9f,stroke:#333
style E fill:#bbf,stroke:#333
style F fill:#bfb,stroke:#333子類別化無法表達會改變的邏輯。你想要的變化必須在建立物件時就已知,之後無法更改。需要使用條件判斷或委派來表達會變化的邏輯。
Implementor#
**多型訊息(Polymorphic Message)**是在物件程式中表達選擇的基本方式。為了讓訊息發揮選擇的作用,需要有不只一種物件可能接收該訊息。
多次實作同一協定——無論是透過 Java interface 加上 implements 宣告,或是透過子類別加上 extends——是在說:
「從運算某個部分的角度來看,只要發生的事符合程式碼的意圖,具體發生什麼細節無關緊要。」
多型訊息的優勢#
多型訊息的美在於它為系統開啟了變化的可能:
- 如果程式的某個部分將位元組寫入另一個系統,引入抽象的
Socket可以讓 socket 的實作變化而不影響呼叫端的程式碼 - 與程序式表達相比,物件/訊息版本更清晰:分離了意圖的表達(寫一些位元組)與實作(用特定參數呼叫 TCP/IP 堆疊)
- 同時為系統開啟了原始程式設計師未曾想像的未來變化
表達清晰度與彈性的幸運結合,正是物件語言成為主流程式設計典範的原因。但這種珍貴的資源很容易因為用 Java 寫出程序式程式而被浪費。
Inner Class#
有時你需要封裝部分運算,但不想承擔一個完整類別(包含自己的檔案)的成本。宣告小型的**私有類別(Inner Class)**提供了一種低成本的方式,擁有類別的許多好處而不需要所有成本。
- 有些 inner class 只繼承
Object - 有些 inner class 繼承另一個父類別,用於表達只在局部有意義的其他類別的精煉
隱含的外部參照#
Inner class 的一個特性是:它們的實例被建立時,會被秘密地傳入建立它們的物件的副本。當你想存取封閉實例的資料而不想讓兩個類別的關係變得明確時,這很方便:
public class InnerClassExample {
private String field;
public class Inner {
public String example() {
return field; // 使用封閉實例的欄位
}
}
@Test public void passes() {
field = "abc";
Inner bar = new Inner();
assertEquals("abc", bar.example());
}
}反射的陷阱#
然而,在上面的 inner class 中,實際上並不存在無參數建構子,即使你宣告了一個。這在使用反射建立 inner class 實例時會造成問題:
public class InnerClassExample {
public class Inner {
public Inner() {
}
}
@Test(expected=NoSuchMethodException.class)
public void innerHasNoNoArgConstructor() throws Exception {
Inner.class.getConstructor(new Class[0]);
}
}若需要一個與封閉實例完全脫離的 inner class,將其宣告為
static。
Instance-Specific Behavior#
理論上,一個類別的所有實例共享相同的邏輯。放寬這個限制可以啟用新的表達風格,但所有這些風格都有成本:
- 當物件的邏輯完全由其類別決定時,讀者可以閱讀類別中的程式碼來了解會發生什麼
- 一旦有了實例特定行為,你必須查看實際執行的範例或分析資料流,才能理解特定物件的行為
為了降低程式碼閱讀的成本,盡量在物件建立時設定實例特定行為,之後不再改變。如果邏輯隨運算進展而改變,閱讀成本會再上升一個層級。
flowchart LR
A["Conditional\n條件判斷"] --> B["Delegation\n委派"]
B --> C["Subclass\n子類別"]
A -.- A1["✅ 邏輯集中\n✅ 簡單直接\n❌ 難以擴展"]
B -.- B1["✅ 運行時可變\n✅ 可獨立測試\n❌ 需要導航多類別"]
C -.- C1["✅ 型別安全\n✅ 清晰的變化點\n❌ 建立後不可變"]Conditional#
if/then 和 switch 語句是實例特定行為的最簡單形式。使用條件判斷,不同的物件會根據其資料執行不同的邏輯。
優勢#
- 邏輯仍然全部在一個類別中
- 讀者不需要四處導航來尋找運算的可能路徑
劣勢#
- 條件判斷無法在不修改物件程式碼的情況下被修改
- 程式中每條執行路徑都有一定的正確機率;路徑越多,程式正確的可能性越低
- 條件判斷的增殖降低了可靠性
重複條件判斷的問題#
考慮一個簡單的圖形編輯器,圖形需要 display() 方法:
public void display() {
switch (getType()) {
case RECTANGLE :
//...
break;
case OVAL :
//...
break;
case TEXT :
//...
break;
default :
break;
}
}圖形還需要判斷某個點是否包含在其中的方法:
public boolean contains(Point p) {
switch (getType()) {
case RECTANGLE :
//...
break;
case OVAL :
//...
break;
case TEXT :
//...
break;
default :
break;
}
}若要新增一種圖形:
- 必須在每個 switch 語句中新增一個分支
- 必須修改
Figure類別,讓所有現有功能面臨風險 - 每個想新增圖形的人都必須協調對同一個類別的變更
解法#
這些問題都可以透過將條件邏輯轉換為訊息來消除——使用子類別或委派(視程式碼情況而定):
- 重複的條件邏輯
- 不同分支的處理差異很大的邏輯
- 經常變更的條件邏輯
以上通常更適合以訊息而非明確邏輯來表達。

Figure 5.6: Conditional logic represented by subclasses and delegation
條件判斷的優勢——簡單且局部——在被過度使用時反而成為負債。
Delegation#
另一種在不同實例中執行不同邏輯的方式是將工作**委派(Delegate)**給多種可能的物件之一。共同邏輯保留在引用方的類別中,變化的部分在被委派者中。
範例:圖形編輯器的工具處理#
在圖形編輯器中,按下按鈕有時表示「建立矩形」,有時表示「移動圖形」等。
用條件邏輯表達:
public void mouseDown() {
switch (getTool()) {
case SELECTING :
//...
break;
case CREATING_RECTANGLE :
//...
break;
case EDITING_TEXT :
//...
break;
default :
break;
}
}這有上述條件判斷的所有問題。子類別化也不能直接解決,因為編輯器需要在生命週期中切換工具。委派允許這種彈性:
public void mouseDown() {
getTool().mouseDown();
}原本在 switch 子句中的程式碼被移到各個工具中。新工具可以在不修改編輯器或現有工具程式碼的情況下引入。但閱讀程式碼需要更多導航,因為 mouse-down 邏輯散布在多個類別中。
委派的變體#
- Pluggable Object:委派者儲存在欄位中
- 動態計算:委派者在需要時即時計算。例如 JUnit 4 動態計算執行測試的物件——舊式測試用一個委派者,新式測試用另一個,這是條件邏輯與委派的混合
委派也可用於程式碼共享,而非只是實例特定行為。
傳遞委派者作為參數#
委派的常見變體是將委派者(delegator)作為參數傳給被委派的方法:
// GraphicEditor
public void mouseDown() {
tool.mouseDown(this);
}
// RectangleTool
public void mouseDown(GraphicEditor editor) {
editor.add(new RectangleFigure());
}如果被委派者需要對「自己」發送訊息,「自己」是模糊的——有時應發給委派物件,有時應發給被委派者。上例中 RectangleTool 新增圖形是加到委派的 GraphicsEditor,而非自己。
反向參照#
GraphicsEditor 可以作為參數傳遞,也可以儲存為永久的反向參照:
// GraphicEditor
public void mouseDown() {
tool.mouseDown();
}
// RectangleTool
private GraphicEditor editor;
public RectangleTool(GraphicEditor editor) {
this.editor = editor;
}
public void mouseDown() {
editor.add(new RectangleFigure());
}傳遞參數使同一工具可用於多個編輯器;如果這不重要,反向指標的程式碼可能更簡單。
Pluggable Selector#
假設你需要實例特定行為,但只針對一兩個方法,而且不介意所有變體的程式碼都在同一個類別中。在這種情況下,將要呼叫的方法名稱存在欄位中,並透過**反射(Reflection)**呼叫該方法。
最初在 JUnit 中,每個測試必須存在自己的類別中,每個子類別只有一個方法。類別在概念上顯得太「重」了。

Figure 5.7: Trivial subclasses to represent different tests
透過實作泛用的 runTest(),不同名稱的測試會執行不同的測試方法。測試名稱被假設為也是一個方法名稱,在測試執行時透過反射取得並執行:
String name;
public void runTest() throws Exception {
Class[] noArguments = new Class[0];
Method method = getClass().getMethod(name, noArguments);
method.invoke(this, new Object[0]);
}簡化後的類別階層使用單一類別:

Figure 5.8: Pluggable selector helps pack tests into a single class
如同所有程式碼壓縮技巧,修改後的程式碼只有在你理解這個「技巧」時才容易閱讀。
當 pluggable selector 首次廣為人知時,人們傾向過度使用。你可能看著某段程式碼,判斷它不可能被呼叫而刪除,結果系統壞掉了——因為它是被某處的 pluggable selector 呼叫的。使用 pluggable selector 的成本相當高,但有限度地使用來解決困難問題可能值得。
Anonymous Inner Class#
Java 提供了另一種實例特定行為的替代方案:匿名內部類別(Anonymous Inner Class)。概念是建立一個只在一個地方使用的類別,可以覆寫一個或多個方法以達到嚴格的局部目的。因為只在一個地方使用,類別可以被隱含地引用而不需要名稱。
有效使用的前提#
- 需要極簡單的 API——例如實作只有一個
run()方法的Runnable - 或者有一個父類別提供大部分所需實作,匿名內部類別只需簡單地實作即可
匿名內部類別的程式碼會打斷它所嵌入的程式碼的呈現,因此需要保持簡短以免分散讀者注意力。
限制#
- 實例中設定的程式碼必須在撰寫類別時就已知(不像委派可以後來新增)
- 實例建立後無法更改
- 難以直接測試,因此不應包含複雜邏輯
- 因為沒有名稱,你無法透過精心選擇的名稱來表達意圖
Library Class#
不屬於任何物件的功能該放哪裡?一個解法是在一個空類別上建立靜態方法。沒有人會建立這個類別的實例——它只是作為函式庫中函式的容器。
限制與改善#
Library class 相當常見,但擴展性不好。將所有邏輯放在靜態方法中,放棄了物件程式設計最大的優勢:一個私有的共享資料命名空間,可以用來簡化邏輯。盡可能將 library class 轉換為物件。
有時這很簡單——只需為方法找到更好的歸屬。例如 Collections library class 有一個 sort(List) 方法,這麼具體的參數暗示這個方法可能屬於 List。
漸進式轉換步驟#
將 library class 漸進轉換為物件的方法:
步驟一:將靜態方法轉為實例方法,先維持相同介面讓靜態方法委派給實例方法:
public static void method(...params...) {
new Library().instanceMethod(...params...);
}
private void instanceMethod(...params...) {
...some logic...
}步驟二:如果多個方法有相似的參數列表(如果沒有,它們可能屬於不同類別),將方法參數轉為建構子參數:
public static void method(...params...) {
new Library(...params...).instanceMethod();
}
private void instanceMethod() {
...some logic...
}步驟三:將實例建立移到客戶端並消除靜態方法:
public void instanceMethod(...params...) {
...some logic...
}這個過程可能會給你靈感,去重新命名類別和方法,讓客戶端程式碼讀起來更清晰。
結論#
類別將相關的狀態綁在一起。下一章將介紹有關狀態(State)的溝通決策模式。