Composite
December 29, 2021假設你今天要開發一個動畫編輯程式,動畫由圖片組成,數張圖片組合為動畫清單,動畫清單也可以由其他已完成的動畫清單組成,你可能這麼設計:
import java.util.*;
class Image {
...
}
class Playlist {
private List<Image> images = new ArrayList<>();
void add(Image Image) {
images.add(Image);
}
void add(Playlist playlist) {
for(Image Image : playlist.images) {
add(Image);
}
}
}
確實地,這樣是解決了需求,不過,如果有個動畫清單是由數個動畫清單組成,現在想從中取出或者移除某個動畫清單,這種方式就行不通了,因為被加入的動畫清單,其中的圖片會被取出,最後你就不知哪個圖片來自哪個動畫清單了。
另一方面,如果今天想在清單中加入影片,Playlist
增加個 add(Video video)
嗎?明天又想加入什麼其他素材呢?再加個 add(XXX xxx)
?
零件/整體
仔細想想,無論是圖片、動畫清單、影片等,都是素材,那就這麼設計吧!
import java.util.*;
interface Material {
}
class Image implements Material {
...
}
class Playlist implements Material {
private List<Material> materials = new ArrayList<>();
public void add(Material material) {
materials.add(material);
}
}
這麼一來,不管 Image
想加入 Playlist
:
var main = new Playlist("主清單");
main.add(new Image("Duke 左揮手"));
main.add(new Image("Duke 右揮手"));
或是 Playlist
想加入 Playlist
:
var walking = new Playlist("走路清單");
walking.add(new Image("Duke 走左腳"));
walking.add(new Image("Duke 走右腳"));
main.add(walking);
或者又多個 Image
:
main.add(new Image("片尾"));
甚至增加個 Video
都不成問題:
class Video implements Material {
...
}
main.add(new Video("幕後花絮"));
如果想要移除或取得素材呢?就以上的範例來說,因為建立 Playlist
、Image
或 Video
時有指定名稱,或許可以定義 remove(String name)
、get(String name)
方法,其中走訪 materials
,根據名稱來刪除或取得,當然,若要更有效率一些,或許 materials
可以改用 Map
。
在這樣將零件/整體一視同仁看待的概念,Gof 稱為 Composite 模式,當物件組合時具有遞迴性,例如這邊的播放軟體範例,或者是測試框架(測試案例與套件)、視窗程式(容器與元件)、需要用樹狀表示的資料結構等,就可以用 Composite 模式作為一個思考的方向。
一視同仁?
不過,你可能會說,哪裡一視同仁了?Playlist
有 add
方法,然而其他類別沒有啊?嗯…這是看你從哪個角度來看,如果從客戶端操作 Playlist
,就可以新增 Playlist
、Image
、Video
來看,是一視同仁沒錯。
不過,通常會採用 Composite 模式來組合的物件,還會定義一些共同行為,以方才的範例來說,或許這個行為是播放:
import java.util.*;
interface Material {
void play(); // 定義共同的播放行為
}
class Image implements Material {
...
public void play() {
...實作播放
}
}
class Playlist implements Material {
private List<Material> materials = new ArrayList<>();
public void add(Material material) {
materials.add(material);
}
public void play() {
for(Material material : materials) {
material.play();
}
}
}
這麼一來,就方才的 all
清單來說,要全部播放的話只要 all.play()
就可以了,就客戶端角度來看,一視同仁的行為是指播放,因為不用管 all
裡頭到底管理的是哪個物件。
一開始我特意不加入 play
的定義,是因為 Gof 在談 Composite 時,是歸類在結構,而結構主要是從物件與物件間組裝的角度來觀察、討論。
就以上的播放軟體來說,Playlist
才具有 add
這類組裝物件的方法,這意謂著它才是具有管理物件的職責角色,視你構築的軟體特性而定,有時你會想要加以區別,例如,若你設計一個視窗程式框架,Window
會是個容器,其中可以裝 Button
、TextField
等,然而 Button
你不想要它是個容器,單純就當個被按的角色,Button
就是沒有管理用的方法,就不會被誤用來當成容器。
有些樹狀結構,每個節點可能就會包含管理物件的行為,任何節點都可以是子樹的根節點,而任何節點也能成為葉節點,例如,也許你想設計另一種視窗程式框架,功能更豐富,就算是 Button
,也可以當個容器,例如,裡頭還可以承載 Image
,變成一個圖片按鈕之類,那麼你可能會想讓 Button
也實作管理物件的職責:
interface Component {
void add(Component component);
void remove(Component component);
void paint(g: Graphics);
}
class Container implements Component {
...實作 add 等方法
}
class Button extends Container {
...實作 Button 需要的行為
}
這時從客戶端的角度來看,一視同仁的行為就包含了管理物件的 add
、remove
,以及繪製元件時的 paint
方法了。
其實也不用太在意從誰的角度看有沒有一視同仁,重點是要搞清楚任務是什麼?為什麼一開始的實作方式行不通?是因為滿足不了什麼需求?重構的方向是什麼?後來採用的方式是不是能達成任務?
確實地,有時我們會透過書或文件來認識模式,以便擴展、吸收一些前人的經驗;然而,透過這種方式認識模式時,如果作者沒談到方才的這幾個問題,也務必要自行思考、自問這些問題!