深入剖析 Java 中的委派模式(Delegation)【Thinking in Java筆記(6-2)】
對剛接觸 Java 的初學者來說,當你寫到第六章,意識到「繼承」有它的便利,但也隱藏著難以掌控的耦合風險,如果想部分重用功能,又不想被不需要的方法干擾,該如何在設計時保持彈性與可維護性?
本文將從「委派模式」的核心概念出發,透過多個真實場景的比喻、逐步拆解的程式碼範例,並輔以常見錯誤排解與最佳實務,引導你靈活運用委派,打造易擴充、低耦合的程式結構
什麼是委派模式?
委派模式(Delegation Pattern)是一種結構型設計模式,讓一個物件將其部分行為委託給另一個類別的實例來執行,而非直接繼承父類的所有行為。它的核心在於「物件組合(Composition)」與「接口包裝(Wrapper)」
實戰範例
委派的實現方式
想像飛船駕駛艙擁有油門、方向盤、推進器等多種控制介面;使用繼承時,就像把整個儀表板直接丟給子類。結果駕駛員得面對一大堆不相關的按鈕,而我們實際只想控制「前進」「後退」等少數功能
繼承作法
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
void forward(int velocity) {}
void back(int velocity) {}
void turboBoost() {}
}
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) { this.name = name; }
public String toString() { return name; }
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}
- 語意不清:繼承暗示
SpaceShip
是一種SpaceShipControls
,讓人誤以為飛船本身具備控制器的行為。但在現實中,飛船只是「使用」這組控制功能;繼承扭曲了設計意圖,也讓程式結構不符合真實需求,日後要擴充或維護時,常常因為不符合直覺而出錯 - 介面過度暴露:繼承機制會將父類別的所有方法一併攤平到子類,導致使用者在呼叫
SpaceShip
時,不僅能使用forward()
、back()
,也能隨意呼叫up()
、down()
、left()
、right()
、turboBoost()
等本不想公開的功能,介面混亂且易引發誤用 - 未來擴充困難:如果想為
turboBoost
增加額外參數或行為,必須直接修改SpaceShipControls
類別本身,這不僅違反封裝原則,還可能影響到所有繼承或依賴該類別的程式碼,增加維護風險與出錯機會
使用委派模式改寫
透過委派,我們可以改寫上述範例,使 SpaceShip
包含一個 SpaceShipControls
的實例,並選擇性地暴露需要的方法。
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// 委派方法
public void back(int velocity) { controls.back(velocity); }
public void down(int velocity) { controls.down(velocity); }
public void forward(int velocity) { controls.forward(velocity); }
public void left(int velocity) { controls.left(velocity); }
public void right(int velocity) { controls.right(velocity); }
public void turboBoost() { controls.turboBoost(); }
public void up(int velocity) { controls.up(velocity); }
public static void main(String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}
在這個改寫的範例中:
- 建立控制器實例:在
SpaceShipDelegation
類別的建構子中,呼叫new SpaceShipControls()
,並將結果賦值給controls
欄位。這樣就完成了「擁有一個控制器」的基礎組合 - 命名與初始化:同步在建構子中接收並設定
name
參數,讓每艘飛船都有自己的辨識名稱 - 撰寫委派方法:以
public void forward(int velocity)
為例,方法內部僅呼叫controls.forward(velocity)
,這一步看似多了一層包裝,但核心優勢在於:你可以控制哪些功能要對外公開如果稍後想把飛船改成『隱形飛船』,也可以在這裡加入額外邏輯,例如先檢查能量值,再執行controls.forward()
- 管理 API 範圍:只把
forward()
、back()
、up()
…等你需要的方法寫進SpaceShipDelegation
。未寫的控制功能(像turboBoost()
)就自動隱藏,不會暴露給使用者 - 呼叫與執行:在
main()
方法裡,只和SpaceShipDelegation
互動:
SpaceShipDelegation ship = new SpaceShipDelegation("NSEA Protector");
ship.forward(100);
使用者根本不需要知道底層 SpaceShipControls
存在,簡潔、直覺
想像你有一台功能強大的遙控車(含燈光、水炮、收音機),但朋友只想玩前進和後退。你不會把整台遙控器直接給他,而是特製一個簡化版遙控器,只留下「前進」「後退」兩顆按鍵。這就是委派:在封裝好複雜內部機制的同時,只對外顯示最核心的操作介面
深入理解委派模式
當你呼叫 SpaceShipDelegation.forward(100)
時,真正執行工作的其實是內部的 controls.forward(100)
——就像透過電話操控智慧家電,你說「開燈」,電話那頭的開關才真正動作。委派模式的威力就在於:外部接口維持不變,但內部實作可以隨時替換或增強
- 組合取代繼承:用一個
controls
物件來承擔控制邏輯,而不是讓SpaceShip
繼承所有方法,避免將整個工具箱交給你手忙腳亂 - 專注核心 API:只在
SpaceShipDelegation
中公開forward()
、back()
、up()
等必要方法,其餘功能保持私有,介面乾淨且易於理解 - 彈性更高:未來若要升級控制器,只要把
controls
換成新版本,就能一次帶來改進,呼叫程式碼完全不改變
優點
- 單一焦點:只公開你需要的方法,不會被其他不相關功能干擾
- 避免過度繼承:在某些情況下,繼承會導致類別之間的耦合度過高,使用委派可以降低這種風險
- 彈性擴充:以後想新增功能,只要在類別中再寫一個委派方法,不用動到原本的控制器類
缺點
- 額外的撰寫工作:需要手動撰寫委派的方法,可能會增加程式碼的量
- 效能開銷:每次方法調用都需要經過一層轉發,可能會有輕微的效能損耗
何時使用委派模式?
- 需要更好的控制權:當你需要對某些方法的存取進行限制或改變其行為時
- 避免不必要的繼承:當繼承會導致語意不清或過度耦合時,委派是更好的選擇
- 動態行為改變:當你需要在運行時更改物件的行為時,可以使用委派模式
實際應用範例
想像你正在開發一款音樂播放器 App,它的任務是對應不同使用者的播放需求:有些人喜歡 MP3,有些人下載了 WAV 檔案,甚至還可能遇到 OGG、FLAC 等格式。對初學者來說,如果把所有格式的解碼邏輯硬塞在同一個類別,不僅程式碼複雜,也難以維護
委派模式在此派上用場:你只要在 AudioPlayer
這個主要類別中保留「播放」這件核心功能,然後針對各種不同的解碼方式,化身成獨立的 AudioDecoder
物件來處理。這樣,當未來再新增新格式,或想對現有格式進行優化測試,都只要替換或擴充解碼器,AudioPlayer
本身毫無感知,程式碼一樣簡潔。以下示範如何用委派模式,把解碼的細節委託給不同的解碼器:
interface AudioDecoder {
void decode(String fileName);
}
class MP3Decoder implements AudioDecoder {
public void decode(String fileName) {
System.out.println("Decoding MP3 file: " + fileName);
}
}
class WAVDecoder implements AudioDecoder {
public void decode(String fileName) {
System.out.println("Decoding WAV file: " + fileName);
}
}
public class AudioPlayer {
private AudioDecoder decoder;
public void setDecoder(AudioDecoder decoder) {
this.decoder = decoder;
}
public void play(String fileName) {
decoder.decode(fileName);
System.out.println("Playing audio file: " + fileName);
}
public static void main(String[] args) {
AudioPlayer player = new AudioPlayer();
player.setDecoder(new MP3Decoder());
player.play("song.mp3");
player.setDecoder(new WAVDecoder());
player.play("song.wav");
}
}
在這個範例中:
- 委派物件(客戶端):
AudioPlayer
不直接實作解碼邏輯,而是負責接收播放請求,並將解碼工作「交給」另一個物件處理。這讓AudioPlayer
顯得乾淨俐落,只專注在「播放流程」的協調 - 解碼介面(抽象契約):
AudioDecoder
定義了decode(String fileName)
方法,是所有解碼器必須實現的契約。任何符合此接口的類別(如MP3Decoder
、WAVDecoder
)都能被AudioPlayer
委派使用 - 運行時切換(高彈性):透過
setDecoder(AudioDecoder decoder)
,程式在執行期間即可替換不同解碼器。例如要支援 OGG,只需實作OggDecoder
並呼叫player.setDecoder(new OggDecoder())
,不需改動AudioPlayer
的任何碼 - 分離關注(Separation of Concerns):
AudioPlayer
只管何時呼叫decoder.decode()
、何時顯示播放訊息;AudioDecoder
則專注於解碼流程,互不干擾,降低維護成本 - 實際成效:
- 新格式上線:只加一個新的
AudioDecoder
類別,老程式瞬間支援 - 測試容易:在單元測試中,給
AudioPlayer
傳入模擬的AudioDecoder
,就能測試播放流程,不用真的跑解碼程式
委派模式有效地將「播放」與「解碼」兩條邏輯分開包裝,讓系統更具可讀性、可維護性,也方便未來擴充各種音訊格式
小結
到這裡,你已經學會如何用委派模式,把「核心功能」和「輔助邏輯」分開包裝,讓程式碼清爽又好讀。記得:
- 💡 拿捏重點:用委派,你只要挑真正需要的方法,讓類別看起來簡潔,使用起來也不會手忙腳亂
- 💡 輕鬆維護:當需求變更時,只要替換或新增一個小小的委派物件,主程式幾乎不動,省下大量找 bug 的時間
- 💡 隨心擴充:想加新功能?在自己的類裡偷偷加個包裝就好,底層邏輯不被波及,不怕手滑改壞了別人寫的程式
Comments ()