Java設計模式-16、命令模式-請求傳送者與接收者解耦

植樹chen發表於2020-11-12

第16章:命令模式-請求傳送者與接收者解耦

定義:

命令模式(Command Pattern):將一個請求封裝為一個物件,從而讓我們可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及支援可撤銷的操作

開關既可以控制燈,也可以控制排氣扇

開關(請求傳送者)--------電器(請求接收者

image-20201112155257722

結構:

image-20201112161252175

程式碼實現:

//抽象命令
abstract class Command {
    public abstract void 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();
    }
}
//請求接收者
class Receiver {
    public void action() {
        //具體操作
    }
}
class ConcreteCommand extends Command {
    private Receiver receiver; //維持一個對請求接收者物件的引用

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

應用例項:

桌面版應用程式,使用者可以自定義功能鍵

image-20201112155541327

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

    //在FunctionButton的onClick()方法中呼叫HelpHandler的display()方法
    public void onClick() {
        help = new HelpHandler();
        help.display(); //顯示幫助文件
    }
}
問題:
  1. 直接呼叫耦合度高,需要修改接收者,必須修改原始碼,違背開閉原則
  2. 增加請求接收者,需要增加功能類
  3. 功能確定,無法設定某個功能鍵的功能

image-20201112162405442

//功能鍵設定視窗類
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("顯示幫助文件!");
    }
}
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();
    }
}

XMLUtil

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("demo/src/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;
        }
    }
}

配置檔案

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

image-20201112171347161

命令佇列實現

多個請求接收者產生響應時使用

新增一個類,CommandQueue負責儲存多個命令物件

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();
        }
    }
}
//請求傳送者
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();
    }
}

撤銷操作實現

計算器,可以實現數學運算和撤銷運算的操作

image-20201112175357403

//加法類:請求接收者
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();
    }
}

image-20201112180102967

請求日誌

  1. 儲存請求的歷史記錄
  2. 將命令佇列中的所有命令物件都儲存在一個日誌檔案中,每執行一個命令則從日誌檔案中刪除一個對應的命令物件,實現批處理

網站配置管理工具,實現增刪改

image-20201112180626853

//抽象命令類,由於需要將命令物件寫入檔案,因此它實現了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);
    }

}
//配置檔案操作類:請求接收者。由於ConfigOperator類的物件是Command的成員物件,它也將隨Command
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();
    }
}

image-20201112181617378

巨集命令

  1. 組合模式和命令模式聯用的產物,巨集命令是一個具體命令類
  2. 通常巨集命令不直接與請求接收者互動,而是通過它的成員來呼叫接收者的方法

image-20201112192815727

優點:

  1. 降低系統的耦合度。由於請求者與接收者之間不存在直接引用,因此請求者與接收者之間實現完全解耦,相同的請求者可以對應不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨立性。
  2. 新的命令可以很容易地加入到系統中。由於增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統原始碼,甚至客戶類程式碼,滿足“開閉原則”的要求。
  3. 可以比較容易地設計一個命令佇列或巨集命令(組合命令)。
  4. 為請求的撤銷(Undo)和恢復(Redo)操作提供了一種設計和實現方案。

缺點:

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

適用場景:

  1. 系統需要將請求呼叫者和請求接收者解耦,使得呼叫者和接收者不直接互動。請求呼叫者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心何時被呼叫。
  2. 系統需要在不同的時間指定請求、將請求排隊和執行請求。一個命令物件和請求的初始呼叫者可以有不同的生命期,換言之,最初的請求發出者可能已經不在了,而命令物件本身仍然是活動的,可以通過該命令物件去呼叫請求接收者,而無須關心請求呼叫者的存在性,可以通過請求日誌檔案等機制來具體實現。
  3. 系統需要支援命令的撤銷(Undo)操作和恢復(Redo)操作。
  4. 系統需要將一組操作組合在一起形成巨集命令。

相關文章