Command 模式


您們團隊在開發影像編輯軟體,您負責的是開發影像處理API,例如:
class Drawing {
void processSome() {
System.out.println(" - 對圖片作 Some 處理");
}
void processOther() {
System.out.println(" - 對圖片作 Other 處理");
}
void processAnother() {
System.out.println(" - 對圖片作 Another 處理");
}
}

A同事告訴您,他想要有個現成的A特效指令,可以組合某幾個影像處理API來完成,所以您 提供了一個ImageService,當作特效指令的執行器:
class ImageService {
private Drawing drawing = new Drawing();
void doAEffect() {
System.out.println("作 A 特效");
drawing.processSome();
drawing.processOther();
}
}

B同事告訴您,他也想要有個現成的B特效指令,可以組合某幾個影像處理API來 完成,所以您也提供了:
class ImageService {
private Drawing drawing = new Drawing();
void doAEffect() {
System.out.println("作 A 特效");
drawing.processSome();
drawing.processOther();
}
void doBEffect() {
System.out.println("作 B 特效");
drawing.processOther();
drawing.processAnother();
}
}

C 同事告訴您,他也想要有個C特效指令,D同事告訴您他要D特效指令...所以您在ImageService中加入了doCEffect()、 doDEffect ()...這樣下去會沒完沒了,問題很明顯,您的ImageService的公開協定越來越多了,因為這樣的ImageService同時了負責特效指令 的建 立與執行,只要有新的特效指令,您就需要修改ImageService。

您沒有辦法預測哪些同事會有哪些特效指令需求,您應該將特效指令的建立職責交給他們,您只負責執行特效指令。採取以下的設計會比較好:
import java.util.*;

interface Drawing {
void processSome();
void processOther();
void processAnother();
}

class DrawingImpl implements Drawing{
public void processSome() {
System.out.println(" - 對圖片作 Some 處理");
}
public void processOther() {
System.out.println(" - 對圖片作 Other 處理");
}
public void processAnother() {
System.out.println(" - 對圖片作 Another 處理");
}
}

interface Command {
void execute(Drawing drawing);
}

class ImageService {
private Map<String, Command> commands = new HashMap<String, Command>();
private Drawing drawing = new DrawingImpl();
void addCommand(String effect, Command command) {
commands.put(effect, command);
}
void doEffect(String effect) {
commands.get(effect).execute(drawing);
}
}

每個同事可以各自設計自己想要的特效指令:
class AEffectCommand implements Command {
public void execute(Drawing drawing) {
System.out.println("作 A 特效");
drawing.processSome();
drawing.processOther();
}
}

class BEffectCommand implements Command {
public void execute(Drawing drawing) {
System.out.println("作 B 特效");
drawing.processOther();
drawing.processAnother();
}
}

class CEffectCommand implements Command {
public void execute(Drawing drawing) {
System.out.println("作 C 特效");
drawing.processOther();
drawing.processSome();
drawing.processAnother();
}
}

影像編輯軟體現在將特效指令逐一加入您提供的ImageService指令執行器,並在需要的時候指定使用該特效:
public class Main {
public static void main(String[] args) {
ImageService service = new ImageService();
service.addCommand("AEffect", new AEffectCommand());
service.addCommand("BEffect", new BEffectCommand());
service.addCommand("CEffect", new CEffectCommand());
// 在需要時指定使用某特效
service.doEffect("AEffect");
service.doEffect("BEffect");
service.doEffect("CEffect");
}
}

使用Python來示範程式範例:
class Drawing:
def processSome(self):
print(" - 對圖片作 Some 處理")
def processOther(self):
print(" - 對圖片作 Other 處理")
def processAnother(self):
print(" - 對圖片作 Another 處理")

class ImageService:
def __init__(self):
self.commands = {}
self.drawing = Drawing()

def addCommand(self, effect, command):
self.commands[effect] = command

def doEffect(self, effect):
self.commands[effect].execute(self.drawing)

class AEffectCommand:
def execute(self, drawing):
print("作 A 特效")
drawing.processSome()
drawing.processOther()

class BEffectCommand:
def execute(self, drawing):
print("作 B 特效")
drawing.processOther()
drawing.processAnother()

class CEffectCommand:
def execute(self, drawing):
print("作 C 特效")
drawing.processOther()
drawing.processSome()
drawing.processAnother()

service = ImageService()
service.addCommand("AEffect", AEffectCommand())
service.addCommand("BEffect", BEffectCommand())
service.addCommand("CEffect", CEffectCommand())

service.doEffect("AEffect")
service.doEffect("BEffect")
service.doEffect("CEffect")

這 是Command設計模式的一個實現,其主要精神在於將指令的建立與執行分離, 要分離的原因可能有多種:
  • 事先無法預測或不想規範指令執行器的客戶端之指令內容。
  • 隔離(客戶端)建立指令時必須的參數(物件)。
  • 隔離(執行器端)執行指令物件時必要的資源。


一般對Command模式的誤解,是將重點放在指令執行器的實現方式(多 數誤解為執行器的實現就是一個指令對應一個名稱)Command的 重點是觀察原本同時擔任指令建立與執行職責的指令執行器,嘗試建立指令物件的公開介面,將指令的建立職責從原本的指令執行器中分離出來,因此指令執行器的 實現方式不會是重點,原本同時負有雙重職責的的指令執行器形式不同,重構為Command模式後的執行器就會有所不同。

所以實現Command模式的指令執行器,其接受指令與執行的方式,未必像上例ImageService的實現方式。

例如Java的視窗程式,在設計視窗程式框架時,設計 人員不可能知道某個事件發生後,使用者想要執行的動作是什麼的,他們採用了Command模式,一個接受指令物件(也就是具事件處理器的物件)的範例可能 像是這個樣子:
menuCut.addActionListener(
    new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            // textArea 是 JTextArea的一個實例
            textArea.cut();  
         }
    });
 
而執行指令的方式,則是在相關事件發生時,經由事件派遣器來呼叫註冊的指令物件。menuCut是JMenuItem的實例,一但這個JMenuItem 被按下,它就會呼叫actionPerformed()方法,以執行您所建立的指令,UML 類別圖如下所示:


JUnit則是將註冊的測試指令物件逐一取出執行,而非前述ImageService一個指令對應一個名稱的方式,可參考 你 來寫,我來跑 的內容。

下圖以更一般的結構來表示JMenuItem等實現的Command模式。可區分接受Command物件Invoker,被封裝在Command物件中, 在執行指令時真正處理相關事情的Receiver,以及建立Command物件的客戶端:


這是將Receiver封裝在Command的情況(就像ActionListener實作物件封裝了JTextArea實例);Receiver不一定 會直接封裝在Command物件中,也有可能是執行指令時,由指令執行器給予。以先前ImageService的範例來說,ImageService相當 於Invoker角色,傳入Command物件execute()方法的Drawing實例,就是Receiver的角色了。

Command 模式是個相當常見的模式,除了先前談過的例子之外,現在許多Web框架也都採用Command模式來設計,以Struts 1.2.x為例,ActionServlet就相當於Invoker,而Action則相當於Command的角色,至於Receiver角色就是您封裝 在實作的Action執行的物 件