行為型SEQ2 - 命令模式 Command Pattern

【學習難度:★★★☆☆,使用頻率:★★★★☆】
直接出處:命令模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/16
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed

請求發送者與接收者解耦——命令模式(一)

裝修新房的最后幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的打開和關閉,例如電燈或者排氣扇。在購買開關時,我們并不知道它將來到底用于控制什么電器,也就是說,開關與電燈、排氣扇并無直接關系,一個開關在安裝之后可能用來控制電燈,也可能用來控制排氣扇或者其他電器設備。開關與電器之間通過電線建立連接,如果開關打開,則電線通電,電器工作;反之,開關關閉,電線斷電,電器停止工作。相同的開關可以通過不同的電線來控制不同的電器,如圖1所示:

圖1 開關與電燈、排氣扇示意圖!

在圖1中,我們可以將開關理解成一個請求的發送者,用戶通過它來發送一個“開燈”請求,而電燈是“開燈”請求的最終接收者和處理者,在圖中,開關和電燈之間并不存在直接耦合關系,它們通過電線連接在一起,使用不同的電線可以連接不同的請求接收者,只需更換一根電線,相同的發送者(開關)即可對應不同的接收者(電器)。

在軟件開發中也存在很多與開關和電器類似的請求發送者和接收者對象,例如一個按鈕,它可能是一個“關閉窗口”請求的發送者,而按鈕點擊事件處理類則是該請求的接收者。為了降低系統的耦合度,將請求的發送者和接收者解耦,我們可以使用一種被稱之為命令模式的設計模式來設計系統,在命令模式中,發送者與接收者之間引入了新的命令對象(類似圖1中的電線),將發送者的請求封裝在命令對象中,再通過命令對象來調用接收者的方法。本章我們將學習用于將請求發送者和接收者解耦的命令模式。

1 自定義功能鍵

Sunny軟件公司開發人員為公司內部OA系統開發了一個桌面版應用程序,該應用程序為用戶提供了一系列自定義功能鍵,用戶可以通過這些功能鍵來實現一些快捷操作。Sunny軟件公司開發人員通過分析,發現不同的用戶可能會有不同的使用習慣,在設置功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設置為“打開幫助文檔”,有的人則喜歡將該功能鍵設置為“最小化至托盤”,為了讓用戶能夠靈活地進行功能鍵的設置,開發人員提供了一個“功能鍵設置”窗口,該窗口界面如圖2所示:

圖2 “功能鍵設置”界面效果圖

通過如圖2所示界面,用戶可以將功能鍵和相應功能綁定在一起,還可以根據需要來修改功能鍵的設置,而且系統在未來可能還會增加一些新的功能或功能鍵。

Sunny軟件公司某開發人員欲使用如下代碼來實現功能鍵與功能處理類之間的調用關系:

//FunctionButton:功能鍵類,請求發送者  
class FunctionButton {  
    private HelpHandler help; //HelpHandler:幫助文檔處理類,請求接收者  

    //在FunctionButton的onClick()方法中調用HelpHandler的display()方法  
    public void onClick() {  
        help = new HelpHandler();  
        help.display(); //顯示幫助文檔  
    }  
}

在上述代碼中,功能鍵類FunctionButton充當請求的發送者,幫助文檔處理類HelpHandler充當請求的接收者,在發送者FunctionButton的onClick()方法中將調用接收者HelpHandler的display()方法。顯然,如果使用上述代碼,將給系統帶來如下幾個問題:

(1) 由于請求發送者和請求接收者之間存在方法的直接調用,耦合度很高,更換請求接收者必須修改發送者的源代碼,如果需要將請求接收者HelpHandler改為WindowHanlder(窗口處理類),則需要修改FunctionButton的源代碼,違背了“開閉原則”。

(2) FunctionButton類在設計和實現時功能已被固定,如果增加一個新的請求接收者,如果不修改原有的FunctionButton類,則必須增加一個新的與FunctionButton功能類似的類,這將導致系統中類的個數急劇增加。由于請求接收者HelpHandler、WindowHanlder等類之間可能不存在任何關系,它們沒有共同的抽象層,因此也很難依據“依賴倒轉原則”來設計FunctionButton。

(3) 用戶無法按照自己的需要來設置某個功能鍵的功能,一個功能鍵類的功能一旦固定,在不修改源代碼的情況下無法更換其功能,系統缺乏靈活性。

不難得知,所有這些問題的產生都是因為請求發送者FunctionButton類和請求接收者HelpHandler、WindowHanlder等類之間存在直接耦合關系,如何降低請求發送者和接收者之間的耦合度,讓相同的發送者可以對應不同的接收者?這是Sunny軟件公司開發人員在設計“功能鍵設置”模塊時不得不考慮的問題。命令模式正為解決這類問題而誕生,此時,如果我們使用命令模式,可以在一定程度上解決上述問題(注:命令模式無法解決類的個數增加的問題),下面就讓我們正式進入命令模式的學習,看看命令模式到底如何實現請求發送者和接收者解耦。

2 命令模式概述

在軟件開發中,我們經常需要向某些對象發送請求(調用其中的某個或某些方法),但是并不知道請求的接收者是誰,也不知道被請求的操作是哪個,此時,我們特別希望能夠以一種松耦合的方式來設計軟件,使得請求發送者與請求接收者能夠消除彼此之間的耦合,讓對象之間的調用關系更加靈活,可以靈活地指定請求接收者以及被請求的操作。命令模式為此類問題提供了一個較為完美的解決方案。

命令模式可以將請求發送者和接收者完全解耦,發送者與接收者之間沒有直接引用關系,發送請求的對象只需要知道如何發送請求,而不必知道如何完成請求。

命令模式定義如下:

命令模式(Command Pattern):將一個請求封裝為一個對象,從而讓我們可用不同的請求對客戶進行參數化;對請求排隊或者記錄請求日志,以及支持可撤銷的操作。命令模式是一種對象行為型模式,其別名為動作(Action)模式或事務(Transaction)模式。

命令模式的定義比較復雜,提到了很多術語,例如“用不同的請求對客戶進行參數化”、“對請求排隊”,“記錄請求日志”、“支持可撤銷操作”等,在后面我們將對這些術語進行一一講解。

命令模式的核心在于引入了命令類,通過命令類來降低發送者和接收者的耦合度,請求發送者只需指定一個命令對象,再通過命令對象來調用請求接收者的處理方法,其結構如圖3所示:

圖3 命令模式結構圖

在命令模式結構圖中包含如下幾個角色:

  • Command(抽象命令類):抽象命令類一般是一個抽象類或接口,在其中聲明了用于執行請求的execute()等方法,通過這些方法可以調用請求接收者的相關操作。

  • ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中聲明的方法,它對應具體的接收者對象,將接收者對象的動作綁定其中。在實現execute()方法時,將調用接收者對象的相關操作(Action)。

  • Invoker(調用者):調用者即請求發送者,它通過命令對象來執行請求。一個調用者并不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關系。在程序運行時可以將一個具體命令對象注入其中,再調用具體命令對象的execute()方法,從而實現間接調用請求接收者的相關操作。

  • Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

命令模式的本質是對請求進行封裝,一個請求對應于一個命令,將發出命令的責任和執行命令的責任分割開。每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方收到請求,并執行相應的操作。命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的接口,更不必知道請求如何被接收、操作是否被執行、何時被執行,以及是怎么被執行的。

命令模式的關鍵在于引入了抽象命令類,請求發送者針對抽象命令類編程,只有實現了抽象命令類的具體命令才與請求接收者相關聯。在最簡單的抽象命令類中只包含了一個抽象的execute()方法,每個具體命令類將一個Receiver類型的對象作為一個實例變量進行存儲,從而具體指定一個請求的接收者,不同的具體命令類提供了execute()方法的不同實現,并調用不同接收者的請求處理方法。 典型的抽象命令類代碼如下所示:

abstract class Command {  
    public abstract void execute();  
}

對于請求發送者即調用者而言,將針對抽象命令類進行編程,可以通過構造注入或者設值注入的方式在運行時傳入具體命令類對象,并在業務方法中調用命令對象的execute()方法,其典型代碼如下所示:

class Invoker {  
    private Command command;  

    //構造注入  
    public Invoker(Command command) {  
        this.command = command;  
    }  

    //設值注入  
    public void setCommand(Command command) {  
        this.command = command;  
    }  

    //業務方法,用于調用命令類的execute()方法  
    public void call() {  
        command.execute();  
    }  
}

具體命令類繼承了抽象命令類,它與請求接收者相關聯,實現了在抽象命令類中聲明的execute()方法,并在實現時調用接收者的請求響應方法action(),其典型代碼如下所示:

class ConcreteCommand extends Command {  
    private Receiver receiver; //維持一個對請求接收者對象的引用  

    public void execute() {  
        receiver.action(); //調用請求接收者的業務處理方法action()  
    }  
}

請求接收者Receiver類具體實現對請求的業務處理,它提供了action()方法,用于執行與請求相關的操作,其典型代碼如下所示:

class Receiver {  
    public void action() {  
        //具體操作  
    }  
}

思考

一個請求發送者能否對應多個請求接收者?如何實現?

請求發送者與接收者解耦——命令模式(二)

3 完整解決方案

為了降低功能鍵與功能處理類之間的耦合度,讓用戶可以自定義每一個功能鍵的功能,Sunny軟件公司開發人員使用命令模式來設計“自定義功能鍵”模塊,其核心結構如圖4所示:

圖4 自定義功能鍵核心結構圖

在圖4中,FBSettingWindow是“功能鍵設置”界面類,FunctionButton充當請求調用者,Command充當抽象命令類,MinimizeCommand和HelpCommand充當具體命令類,WindowHanlder和HelpHandler充當請求接收者。完整代碼如下所示:

import java.util.*;  

//功能鍵設置窗口類  
class FBSettingWindow {  
    private String title; //窗口標題  
    //定義一個ArrayList來存儲所有功能鍵  
    private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();  

    public FBSettingWindow(String title) {  
        this.title = title;  
    }  

    public void setTitle(String title) {  
        this.title = title;  
    }  

    public String getTitle() {  
        return this.title;  
    }  

    public void addFunctionButton(FunctionButton fb) {  
        functionButtons.add(fb);  
    }  

    public void removeFunctionButton(FunctionButton fb) {  
        functionButtons.remove(fb);  
    }  

    //顯示窗口及功能鍵  
    public void display() {  
        System.out.println("顯示窗口:" + this.title);  
        System.out.println("顯示功能鍵:");  
        for (Object obj : functionButtons) {  
            System.out.println(((FunctionButton)obj).getName());  
        }  
        System.out.println("------------------------------");  
    }     
}  

//功能鍵類:請求發送者  
class FunctionButton {  
    private String name; //功能鍵名稱  
    private Command command; //維持一個抽象命令對象的引用  

    public FunctionButton(String name) {  
        this.name = name;  
    }  

    public String getName() {  
        return this.name;  
    }  

    //為功能鍵注入命令  
    public void setCommand(Command command) {  
        this.command = command;  
    }  

    //發送請求的方法  
    public void onClick() {  
        System.out.print("點擊功能鍵:");  
        command.execute();  
    }  
}  

//抽象命令類  
abstract class Command {  
    public abstract void execute();  
}  

//幫助命令類:具體命令類  
class HelpCommand extends Command {  
    private HelpHandler hhObj; //維持對請求接收者的引用  

    public HelpCommand() {  
        hhObj = new HelpHandler();  
    }  

    //命令執行方法,將調用請求接收者的業務方法  
    public void execute() {  
        hhObj.display();  
    }  
}  

//最小化命令類:具體命令類  
class MinimizeCommand extends Command {  
    private WindowHanlder whObj; //維持對請求接收者的引用  

    public MinimizeCommand() {  
        whObj = new WindowHanlder();  
    }  

//命令執行方法,將調用請求接收者的業務方法  
    public void execute() {  
        whObj.minimize();  
    }  
}  

//窗口處理類:請求接收者  
class WindowHanlder {  
    public void minimize() {  
        System.out.println("將窗口最小化至托盤!");  
    }  
}  

//幫助文檔處理類:請求接收者  
class HelpHandler {  
    public void display() {  
        System.out.println("顯示幫助文檔!");  
    }  
}

為了提高系統的靈活性和可擴展性,我們將具體命令類的類名存儲在配置文件中,并通過工具類XMLUtil來讀取配置文件并反射生成對象,XMLUtil類的代碼如下所示:

import javax.xml.parsers.*;  
import org.w3c.dom.*;  
import org.xml.sax.SAXException;  
import java.io.*;  

public class XMLUtil {  
//該方法用于從XML配置文件中提取具體類類名,并返回一個實例對象,可以通過參數的不同返回不同類名節點所對應的實例  
    public static Object getBean(int i) {  
        try {  
            //創建文檔對象  
            DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();  
            DocumentBuilder builder = dFactory.newDocumentBuilder();  
            Document doc;                             
            doc = builder.parse(new File("config.xml"));   

            //獲取包含類名的文本節點  
            NodeList nl = doc.getElementsByTagName("className");  
            Node classNode = null;  
            if (0 == i) {  
                classNode = nl.item(0).getFirstChild();  
            }  
            else {  
                classNode = nl.item(1).getFirstChild();  
            }   

            String cName = classNode.getNodeValue();  

            //通過類名生成實例對象并將其返回  
            Class c = Class.forName(cName);  
            Object obj = c.newInstance();  
            return obj;  
        }     
        catch(Exception e){  
            e.printStackTrace();  
            return null;  
        }  
    }  
}

配置文件config.xml中存儲了具體建造者類的類名,代碼如下所示:

<?xml version="1.0"?>  
<config>  
    <className>HelpCommand</className>  
    <className>MinimizeCommand</className>  
</config>  

編寫如下客戶端測試代碼:

class Client {  
    public static void main(String args[]) {  
        FBSettingWindow fbsw = new FBSettingWindow("功能鍵設置");  

        FunctionButton fb1,fb2;  
        fb1 = new FunctionButton("功能鍵1");  
        fb2 = new FunctionButton("功能鍵1");  

        Command command1,command2;  
        //通過讀取配置文件和反射生成具體命令對象  
        command1 = (Command)XMLUtil.getBean(0);  
        command2 = (Command)XMLUtil.getBean(1);  

        //將命令對象注入功能鍵  
        fb1.setCommand(command1);  
        fb2.setCommand(command2);  

        fbsw.addFunctionButton(fb1);  
        fbsw.addFunctionButton(fb2);  
        fbsw.display();  

        //調用功能鍵的業務方法  
        fb1.onClick();  
        fb2.onClick();  
    }  
}

編譯并運行程序,輸出結果如下:

顯示窗口:功能鍵設置
顯示功能鍵:
功能鍵1
功能鍵1
------------------------------
點擊功能鍵:顯示幫助文檔!
點擊功能鍵:將窗口最小化至托盤!

如果需要修改功能鍵的功能,例如某個功能鍵可以實現“自動截屏”,只需要對應增加一個新的具體命令類,在該命令類與屏幕處理者(ScreenHandler)之間創建一個關聯關系,然后將該具體命令類的對象通過配置文件注入到某個功能鍵即可,原有代碼無須修改,符合“開閉原則”。在此過程中,每一個具體命令類對應一個請求的處理者(接收者),通過向請求發送者注入不同的具體命令對象可以使得相同的發送者對應不同的接收者,從而實現“將一個請求封裝為一個對象,用不同的請求對客戶進行參數化”,客戶端只需要將具體命令對象作為參數注入請求發送者,無須直接操作請求的接收者。

請求發送者與接收者解耦——命令模式(三)

4 命令隊列的實現

有時候我們需要將多個請求排隊,當一個請求發送者發送一個請求時,將不止一個請求接收者產生響應,這些請求接收者將逐個執行業務方法,完成對請求的處理。此時,我們可以通過命令隊列來實現。

命令隊列的實現方法有多種形式,其中最常用、靈活性最好的一種方式是增加一個CommandQueue類,由該類來負責存儲多個命令對象,而不同的命令對象可以對應不同的請求接收者,CommandQueue類的典型代碼如下所示:

import java.util.*;  

class CommandQueue {  
    //定義一個ArrayList來存儲命令隊列  
    private ArrayList<Command> commands = new ArrayList<Command>();  

    public void addCommand(Command command) {  
        commands.add(command);  
    }  

    public void removeCommand(Command command) {  
        commands.remove(command);  
    }  

    //循環調用每一個命令對象的execute()方法  
    public void execute() {  
        for (Object command : commands) {  
            ((Command)command).execute();  
        }  
    }  
}

在增加了命令隊列類CommandQueue以后,請求發送者類Invoker將針對CommandQueue編程,代碼修改如下:

class Invoker {  
    private CommandQueue commandQueue; //維持一個CommandQueue對象的引用  

    //構造注入  
    public Invoker(CommandQueue commandQueue) {  
        this. commandQueue = commandQueue;  
    }  

    //設值注入  
    public void setCommandQueue(CommandQueue commandQueue) {  
        this.commandQueue = commandQueue;  
    }  

    //調用CommandQueue類的execute()方法  
    public void call() {  
        commandQueue.execute();  
    }  
}

命令隊列與我們常說的“批處理”有點類似。批處理,顧名思義,可以對一組對象(命令)進行批量處理,當一個發送者發送請求后,將有一系列接收者對請求作出響應,命令隊列可以用于設計批處理應用程序,如果請求接收者的接收次序沒有嚴格的先后次序,我們還可以使用多線程技術來并發調用命令對象的execute()方法,從而提高程序的執行效率。

請求發送者與接收者解耦——命令模式(四)

5 撤銷操作的實現

在命令模式中,我們可以通過調用一個命令對象的execute()方法來實現對請求的處理,如果需要撤銷(Undo)請求,可通過在命令類中增加一個逆向操作來實現。

擴展

除了通過一個逆向操作來實現撤銷(Undo)外,還可以通過保存對象的歷史狀態來實現撤銷,后者可使用備忘錄模式(Memento Pattern)來實現。

下面通過一個簡單的實例來學習如何使用命令模式實現撤銷操作:

Sunny軟件公司欲開發一個簡易計算器,該計算器可以實現簡單的數學運算,還可以對運算實施撤銷操作。

Sunny軟件公司開發人員使用命令模式設計了如圖5所示結構圖,其中計算器界面類CalculatorForm充當請求發送者,實現了數據求和功能的加法類Adder充當請求接收者,界面類可間接調用加法類中的add()方法實現加法運算,并且提供了可撤銷加法運算的undo()方法。

圖5 簡易計算器結構圖

本實例完整代碼如下所示:

//加法類:請求接收者  
class Adder {  
    private int num=0; //定義初始值為0  

    //加法操作,每次將傳入的值與num作加法運算,再將結果返回  
    public int add(int value) {  
        num += value;  
        return num;  
    }  
}  

//抽象命令類  
abstract class AbstractCommand {  
    public abstract int execute(int value); //聲明命令執行方法execute()  
    public abstract int undo(); //聲明撤銷方法undo()  
}  

//具體命令類  
class ConcreteCommand extends AbstractCommand {  
    private Adder adder = new Adder();  
    private int value;  

    //實現抽象命令類中聲明的execute()方法,調用加法類的加法操作  
public int execute(int value) {  
        this.value=value;  
        return adder.add(value);  
    }  

    //實現抽象命令類中聲明的undo()方法,通過加一個相反數來實現加法的逆向操作  
    public int undo() {  
        return adder.add(-value);  
    }  
}  

//計算器界面類:請求發送者  
class CalculatorForm {  
    private AbstractCommand command;  

    public void setCommand(AbstractCommand command) {  
        this.command = command;  
    }  

    //調用命令對象的execute()方法執行運算  
    public void compute(int value) {  
        int i = command.execute(value);  
        System.out.println("執行運算,運算結果為:" + i);  
    }  

    //調用命令對象的undo()方法執行撤銷  
    public void undo() {  
        int i = command.undo();  
        System.out.println("執行撤銷,運算結果為:" + i);  
    }  
}

編寫如下客戶端測試代碼:

class Client {  
    public static void main(String args[]) {  
        CalculatorForm form = new CalculatorForm();  
        AbstractCommand command;  
        command = new ConcreteCommand();  
        form.setCommand(command); //向發送者注入命令對象  

        form.compute(10);  
        form.compute(5);  
        form.compute(10);  
        form.undo();  
    }  
}

編譯并運行程序,輸出結果如下:

執行運算,運算結果為:10
執行運算,運算結果為:15
執行運算,運算結果為:25
執行撤銷,運算結果為:15

思考

如果連續調用“form.undo()”兩次,預測客戶端代碼的輸出結果。
需要注意的是在本實例中只能實現一步撤銷操作,因為沒有保存命令對象的歷史狀態,可以通過引入一個命令集合或其他方式來存儲每一次操作時命令的狀態,從而實現多次撤銷操作。除了Undo操作外,還可以采用類似的方式實現恢復(Redo)操作,即恢復所撤銷的操作(或稱為二次撤銷)。

練習

修改簡易計算器源代碼,使之能夠實現多次撤銷(Undo)和恢復(Redo)。

請求發送者與接收者解耦——命令模式(五)

6 請求日志

請求日志就是將請求的歷史記錄保存下來,通常以日志文件(Log File)的形式永久存儲在計算機中。很多系統都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以記錄用戶對系統的一些操作(例如對數據的更改)。請求日志文件可以實現很多功能,常用功能如下:

(1) “天有不測風云”,一旦系統發生故障,日志文件可以為系統提供一種恢復機制,在請求日志文件中可以記錄用戶對系統的每一步操作,從而讓系統能夠順利恢復到某一個特定的狀態;

(2) 請求日志也可以用于實現批處理,在一個請求日志文件中可以存儲一系列命令對象,例如一個命令隊列;

(3) 可以將命令隊列中的所有命令對象都存儲在一個日志文件中,每執行一個命令則從日志文件中刪除一個對應的命令對象,防止因為斷電或者系統重啟等原因造成請求丟失,而且可以避免重新發送全部請求時造成某些命令的重復執行,只需讀取請求日志文件,再繼續執行文件中剩余的命令即可。

在實現請求日志時,我們可以將命令對象通過序列化寫到日志文件中,此時命令類必須實現Java.io.Serializable接口。下面我們通過一個簡單實例來說明日志文件的用途以及如何實現請求日志:

Sunny軟件公司開發了一個網站配置文件管理工具,可以通過一個可視化界面對網站配置文件進行增刪改等操作,該工具使用命令模式進行設計,結構如圖6所示:

圖6 網站配置文件管理工具結構圖

現在Sunny軟件公司開發人員希望將對配置文件的操作請求記錄在日志文件中,如果網站重新部署,只需要執行保存在日志文件中的命令對象即可修改配置文件。

本實例完整代碼如下所示:

import java.io.*;  
import java.util.*;  

//抽象命令類,由于需要將命令對象寫入文件,因此它實現了Serializable接口  
abstract class Command implements Serializable {  
    protected String name; //命令名稱  
    protected String args; //命令參數  
    protected ConfigOperator configOperator; //維持對接收者對象的引用  

    public Command(String name) {  
        this.name = name;  
    }  

    public String getName() {  
        return this.name;  
    }  

    public void setName(String name) {  
        this.name = name;  
    }  

    public void setConfigOperator(ConfigOperator configOperator) {  
        this.configOperator = configOperator;  
    }  

    //聲明兩個抽象的執行方法execute()  
    public abstract void execute(String args);  
    public abstract void execute();  
}  

//增加命令類:具體命令  
class InsertCommand extends Command {  
    public InsertCommand(String name) {  
        super(name);  
    }  

    public void execute(String args) {  
        this.args = args;  
        configOperator.insert(args);  
    }  

    public void execute() {  
        configOperator.insert(this.args);  
    }  
}  

//修改命令類:具體命令  
class ModifyCommand extends Command {  
    public ModifyCommand(String name) {  
        super(name);  
    }  

    public void execute(String args) {  
        this.args = args;  
        configOperator.modify(args);  
    }  

    public void execute() {  
        configOperator.modify(this.args);  
    }  
}  

//省略了刪除命令類DeleteCommand  

//配置文件操作類:請求接收者。由于ConfigOperator類的對象是Command的成員對象,它也將隨Command對象一起寫入文件,因此ConfigOperator也需要實現Serializable接口  
class ConfigOperator implements Serializable {  
    public void insert(String args) {  
        System.out.println("增加新節點:" + args);  
    }  

    public void modify(String args) {  
        System.out.println("修改節點:" + args);  
    }  

    public void delete(String args) {  
        System.out.println("刪除節點:" + args);  
    }  
}  

//配置文件設置窗口類:請求發送者  
class ConfigSettingWindow {  
    //定義一個集合來存儲每一次操作時的命令對象  
    private ArrayList<Command> commands = new ArrayList<Command>();  
    private Command command;   

    //注入具體命令對象  
    public void setCommand(Command command) {  
        this.command = command;  
    }  

    //執行配置文件修改命令,同時將命令對象添加到命令集合中  
    public void call(String args) {  
        command.execute(args);  
        commands.add(command);  
    }  

    //記錄請求日志,生成日志文件,將命令集合寫入日志文件  
    public void save() {  
        FileUtil.writeCommands(commands);  
    }  

    //從日志文件中提取命令集合,并循環調用每一個命令對象的execute()方法來實現配置文件的重新設置  
    public void recover() {  
        ArrayList list;  
        list = FileUtil.readCommands();  

        for (Object obj : list) {  
            ((Command)obj).execute();  
        }  
    }  
}  

//工具類:文件操作類  
class FileUtil {  
    //將命令集合寫入日志文件  
    public static void writeCommands(ArrayList commands) {  
        try {  
            FileOutputStream file = new FileOutputStream("config.log");  
            //創建對象輸出流用于將對象寫入到文件中  
            ObjectOutputStream objout = new ObjectOutputStream(new BufferedOutputStream(file));  
            //將對象寫入文件  
            objout.writeObject(commands);  
            objout.close();  
            }  
        catch(Exception e) {  
                System.out.println("命令保存失敗!");    
                e.printStackTrace();  
            }  
    }  

    //從日志文件中提取命令集合  
    public static ArrayList readCommands() {  
        try {  
            FileInputStream file = new FileInputStream("config.log");  
            //創建對象輸入流用于從文件中讀取對象  
            ObjectInputStream objin = new ObjectInputStream(new BufferedInputStream(file));  

            //將文件中的對象讀出并轉換為ArrayList類型  
            ArrayList commands = (ArrayList)objin.readObject();  
            objin.close();  
            return commands;  
            }  
        catch(Exception e) {  
                System.out.println("命令讀取失敗!");  
                e.printStackTrace();  
                return null;      
            }         
    }  
}

編寫如下客戶端測試代碼:

class Client {  
    public static void main(String args[]) {  
        ConfigSettingWindow csw = new ConfigSettingWindow(); //定義請求發送者  
        Command command; //定義命令對象  
        ConfigOperator co = new ConfigOperator(); //定義請求接收者  

        //四次對配置文件的更改  
        command = new InsertCommand("增加");  
        command.setConfigOperator(co);  
        csw.setCommand(command);  
        csw.call("網站首頁");  

        command = new InsertCommand("增加");  
        command.setConfigOperator(co);  
        csw.setCommand(command);  
        csw.call("端口號");  

        command = new ModifyCommand("修改");  
        command.setConfigOperator(co);  
        csw.setCommand(command);  
        csw.call("網站首頁");  

        command = new ModifyCommand("修改");  
        command.setConfigOperator(co);  
        csw.setCommand(command);          
        csw.call("端口號");  

        System.out.println("----------------------------");  
        System.out.println("保存配置");  
        csw.save();  

        System.out.println("----------------------------");   
        System.out.println("恢復配置");  
        System.out.println("----------------------------");   
        csw.recover();    
    }  
}

編譯并運行程序,輸出結果如下:

增加新節點:網站首頁
增加新節點:端口號
修改節點:網站首頁
修改節點:端口號
----------------------------
保存配置
----------------------------
恢復配置
----------------------------
增加新節點:網站首頁
增加新節點:端口號
修改節點:網站首頁
修改節點:端口號

請求發送者與接收者解耦——命令模式(六)

7 宏命令

宏命令(Macro Command)又稱為組合命令,它是組合模式和命令模式聯用的產物。宏命令是一個具體命令類,它擁有一個集合屬性,在該集合中包含了對其他命令對象的引用。通常宏命令不直接與請求接收者交互,而是通過它的成員來調用接收者的方法。當調用宏命令的execute()方法時,將遞歸調用它所包含的每個成員命令的execute()方法,一個宏命令的成員可以是簡單命令,還可以繼續是宏命令。執行一個宏命令將觸發多個具體命令的執行,從而實現對命令的批處理,其結構如圖7所示:

圖7 宏命令結構圖

8 命令模式總結

命令模式是一種使用頻率非常高的設計模式,它可以將請求發送者與接收者解耦,請求發送者通過命令對象來間接引用請求接收者,使得系統具有更好的靈活性和可擴展性。在基于GUI的軟件開發,無論是在電腦桌面應用還是在移動應用中,命令模式都得到了廣泛的應用。

  1. 主要優點

命令模式的主要優點如下:

(1) 降低系統的耦合度。由于請求者與接收者之間不存在直接引用,因此請求者與接收者之間實現完全解耦,相同的請求者可以對應不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨立性。

(2) 新的命令可以很容易地加入到系統中。由于增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統源代碼,甚至客戶類代碼,滿足“開閉原則”的要求。

(3) 可以比較容易地設計一個命令隊列或宏命令(組合命令)。

(4) 為請求的撤銷(Undo)和恢復(Redo)操作提供了一種設計和實現方案。

  1. 主要缺點

命令模式的主要缺點如下:

使用命令模式可能會導致某些系統有過多的具體命令類。因為針對每一個對請求接收者的調用操作都需要設計一個具體命令類,因此在某些系統中可能需要提供大量的具體命令類,這將影響命令模式的使用。

  1. 適用場景

在以下情況下可以考慮使用命令模式:

(1) 系統需要將請求調用者和請求接收者解耦,使得調用者和接收者不直接交互。請求調用者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心何時被調用。

(2) 系統需要在不同的時間指定請求、將請求排隊和執行請求。一個命令對象和請求的初始調用者可以有不同的生命期,換言之,最初的請求發出者可能已經不在了,而命令對象本身仍然是活動的,可以通過該命令對象去調用請求接收者,而無須關心請求調用者的存在性,可以通過請求日志文件等機制來具體實現。

(3) 系統需要支持命令的撤銷(Undo)操作和恢復(Redo)操作。

(4) 系統需要將一組操作組合在一起形成宏命令。

練習

Sunny軟件公司欲開發一個基于Windows平臺的公告板系統。該系統提供了一個主菜單(Menu),在主菜單中包含了一些菜單項(MenuItem),可以通過Menu類的addMenuItem()方法增加菜單項。菜單項的主要方法是click(),每一個菜單項包含一個抽象命令類,具體命令類包括OpenCommand(打開命令),CreateCommand(新建命令),EditCommand(編輯命令)等,命令類具有一個execute()方法,用于調用公告板系統界面類(BoardScreen)的open()、create()、edit()等方法。試使用命令模式設計該系統,以便降低MenuItem類與BoardScreen類之間的耦合度。

練習會在我的github上做掉

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容

  • 1 場景問題# 1.1 如何開機## 估計有些朋友看到這個標題會非常奇怪,電腦裝配好了,如何開機?不就是按下啟動按...
    七寸知架構閱讀 2,855評論 1 59
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,786評論 18 139
  • 在湖畔的圣林里,有一棵大樹。樹的周圍總有一個令人毛骨悚然的身影獨自徘徊,不論是白天還是黑夜,人們隨時都能看見他。據...
    夏槿11閱讀 9,064評論 5 11
  • 昨天,天陰沉沉的,風呼嘯著穿過宿舍。安靜的寢室里,一直對著屏幕敲鍵盤的我突然轉過頭去,對著同樣對著屏幕敲鍵盤的室友...
    小會小會閱讀 1,062評論 0 2
  • 無意中在一家書店,隨手拿起一本書翻閱,是祥子寫的《把日子過成詩》,開始是被書名所吸引,看了幾頁就被書中的文字深深的...
    Lxyn藍心閱讀 2,667評論 4 3