JAVA设计模式(18) —<行为型>命令模式(Command)

定义:

命令模式(Command),别名为动作(Action)模式或事务(Transaction)模式

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.(将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能)

命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求

1.1 通用类图:


在命令模式结构图中包含如下几个角色:

       ● 抽象命令类(Command):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。

       ● 具体命令类(ConcreteCommand):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)

       ● 调用者(Invoker):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。

       ● 接收者(Receiver):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

       命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的

       命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。

1.2 通用代码:

注意上述client的静态main方法为人为添加的测试,非原模式的一部分。

[java]  view plain copy
  1. public abstract class Receiver {  
  2.         // 抽象接收者,定义每个接收者都必须完成的业务  
  3.         public abstract void action();  
  4.     }  
  5.   
  6.     public class ConcreteReceiver1 extends Receiver {  
  7.         // 每个接受者都必须处理一定的业务逻辑  
  8.         public void action() {  
  9.         }  
  10.     }  
  11.   
  12.     public abstract class Command {  
  13.         // 每个命令类都必须有一个执行命令的方法  
  14.         public abstract void execute();  
  15.     }  
  16.   
  17.     public class ConcreteCommand1 extends Command {  
  18.         // 也对那个Receiver类进行命令处理  
  19.         private Receiver receiver;  
  20.   
  21.         // 构造函数传递接收者  
  22.         public ConcreteCommand1(Receiver _receiver) {  
  23.             this.receiver = _receiver;  
  24.         }  
  25.   
  26.         // 每个具体的命令都必须实现一个命令  
  27.         public void execute() {  
  28.             // 业务处理  
  29.             this.receiver.action();  
  30.         }  
  31.     }  
  32.   
  33.     public class Invoker {  
  34.         private Command command;  
  35.   
  36.         // 受气包,接受命令  
  37.         public void setCommand(Command _command) {  
  38.             this.command = _command;  
  39.         }  
  40.   
  41.         // 执行命令  
  42.         public void doSomething() {  
  43.             this.command.execute();  
  44.         }  
  45.     }  
  46.   
  47.     public class Client {  
  48.         public static Command prepareCommand() {  
  49.             // 定义接收者  
  50.             Receiver receiver = new ConcreteReceiver1();  
  51.             // 定义一个发送给接收者的命令  
  52.             Command command = new ConcreteCommand1(receiver);  
  53.             return command;  
  54.         }  
  55.   
  56.         public static void main(String[] args) {  
  57.             // 首先声明出调用者Invoker  
  58.             Invoker invoker = new Invoker();  
  59.             // 把命令交给调用者去执行  
  60.             invoker.setCommand(prepareCommand());  
  61.             invoker.doSomething();  
  62.         }  
  63.     }  


优点

      命令模式的主要优点如下:

       (1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。

       (2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。

       (3) 可以比较容易地设计一个命令队列或宏命令(组合命令)

       (4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案

缺点

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

       使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。

应用场景

  在以下情况下可以考虑使用命令模式:

       (1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。

       (2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。

       (3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

       (4) 系统需要将一组操作组合在一起形成宏命令。

注意事项

扩展

6.1 命令队列的实现

有时候我们需要将多个请求排队当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。

       命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:

[java]  view plain copy
  1. import java.util.*;  
  2.   
  3. class CommandQueue {  
  4.     //定义一个ArrayList来存储命令队列  
  5.     private ArrayList<Command> commands = new ArrayList<Command>();  
  6.       
  7.     public void addCommand(Command command) {  
  8.         commands.add(command);  
  9.     }  
  10.       
  11.     public void removeCommand(Command command) {  
  12.         commands.remove(command);  
  13.     }  
  14.       
  15.     //循环调用每一个命令对象的execute()方法  
  16.     public void execute() {  
  17.         for (Object command : commands) {  
  18.             ((Command)command).execute();  
  19.         }  
  20.     }  
  21. }  

       在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下:

[java]  view plain copy
  1. class Invoker {  
  2.     private CommandQueue commandQueue; //维持一个CommandQueue对象的引用  
  3.       
  4.     //构造注入  
  5.     public Invoker(CommandQueue commandQueue) {  
  6.         this. commandQueue = commandQueue;  
  7.     }  
  8.       
  9.     //设值注入  
  10.     public void setCommandQueue(CommandQueue commandQueue) {  
  11.         this.commandQueue = commandQueue;  
  12.     }  
  13.       
  14.     //调用CommandQueue类的execute()方法  
  15.     public void call() {  
  16.         commandQueue.execute();  
  17.     }  
  18. }  

       命令队列与我们常说的“批处理”有点类似。批处理,顾名思义,可以对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求作出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,我们还可以使用多线程技术来并发调用命令对象的execute()方法,从而提高程序的执行效

6.2 撤销操作的实现

在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。

微笑

扩展

除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现

       下面通过一个简单的实例来学习如何使用命令模式实现撤销操作:

       Sunny软件公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。

       Sunny软件公司开发人员使用命令模式设计了如图5所示结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。

5  简易计算器结构图

       本实例完整代码如下所示:

[java]  view plain copy
  1. //加法类:请求接收者  
  2. class Adder {  
  3.     private int num=0//定义初始值为0  
  4.       
  5.     //加法操作,每次将传入的值与num作加法运算,再将结果返回  
  6.     public int add(int value) {  
  7.         num += value;  
  8.         return num;  
  9.     }  
  10. }  
  11.   
  12. //抽象命令类  
  13. abstract class AbstractCommand {  
  14.     public abstract int execute(int value); //声明命令执行方法execute()  
  15.     public abstract int undo(); //声明撤销方法undo()  
  16. }  
  17.   
  18. //具体命令类  
  19. class ConcreteCommand extends AbstractCommand {  
  20.     private Adder adder = new Adder();  
  21.     private int value;  
  22.           
  23.     //实现抽象命令类中声明的execute()方法,调用加法类的加法操作  
  24. public int execute(int value) {  
  25.         this.value=value;  
  26.         return adder.add(value);  
  27.     }  
  28.       
  29.     //实现抽象命令类中声明的undo()方法,通过加一个相反数来实现加法的逆向操作  
  30.     public int undo() {  
  31.         return adder.add(-value);  
  32.     }  
  33. }  
  34.   
  35. //计算器界面类:请求发送者  
  36. class CalculatorForm {  
  37.     private AbstractCommand command;  
  38.       
  39.     public void setCommand(AbstractCommand command) {  
  40.         this.command = command;  
  41.     }  
  42.       
  43.     //调用命令对象的execute()方法执行运算  
  44.     public void compute(int value) {  
  45.         int i = command.execute(value);  
  46.         System.out.println("执行运算,运算结果为:" + i);  
  47.     }  
  48.       
  49.     //调用命令对象的undo()方法执行撤销  
  50.     public void undo() {  
  51.         int i = command.undo();  
  52.         System.out.println("执行撤销,运算结果为:" + i);  
  53.     }  
  54. }  

       编写如下客户端测试代码:

[java]  view plain copy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         CalculatorForm form = new CalculatorForm();  
  4.         AbstractCommand command;  
  5.         command = new ConcreteCommand();  
  6.         form.setCommand(command); //向发送者注入命令对象  
  7.           
  8.         form.compute(10);  
  9.         form.compute(5);  
  10.         form.compute(10);  
  11.         form.undo();  
  12.     }  
  13. }  


        编译并运行程序,输出结果如下:

执行运算,运算结果为:10

执行运算,运算结果为:15

执行运算,运算结果为:25

执行撤销,运算结果为:15

 

疑问

思考

如果连续调用“form.undo()”两次,预测客户端代码的输出结果。

       需要注意的是在本实例中只能实现一步撤销操作,因为没有保存命令对象的历史状态,可以通过引入一个命令集合或其他方式来存储每一次操作时命令的状态,从而实现多次撤销操作。除了Undo操作外,还可以采用类似的方式实现恢复(Redo)操作,即恢复所撤销的操作(或称为二次撤销)。

6.3请求日志

请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。很多系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以记录用户对系统的一些操作(例如对数据的更改)。请求日志文件可以实现很多功能,常用功能如下:

       (1) “天有不测风云”,一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一个特定的状态;

       (2) 请求日志也可以用于实现批处理,在一个请求日志文件中可以存储一系列命令对象,例如一个命令队列;

       (3) 可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。

       在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现java.io.Serializable接口。下面我们通过一个简单实例来说明日志文件的用途以及如何实现请求日志:

       Sunny软件公司开发了一个网站配置文件管理工具,可以通过一个可视化界面对网站配置文件进行增删改等操作,该工具使用命令模式进行设计,结构如图6所示:

6  网站配置文件管理工具结构图

       现在Sunny软件公司开发人员希望将对配置文件的操作请求记录在日志文件中,如果网站重新部署,只需要执行保存在日志文件中的命令对象即可修改配置文件。

       本实例完整代码如下所示:

[java]  view plain copy
  1. import java.io.*;  
  2. import java.util.*;  
  3.   
  4. //抽象命令类,由于需要将命令对象写入文件,因此它实现了Serializable接口  
  5. abstract class Command implements Serializable {  
  6.     protected String name; //命令名称  
  7.     protected String args; //命令参数  
  8.     protected ConfigOperator configOperator; //维持对接收者对象的引用  
  9.       
  10.     public Command(String name) {  
  11.         this.name = name;  
  12.     }  
  13.       
  14.     public String getName() {  
  15.         return this.name;  
  16.     }  
  17.       
  18.     public void setName(String name) {  
  19.         this.name = name;  
  20.     }  
  21.       
  22.     public void setConfigOperator(ConfigOperator configOperator) {  
  23.         this.configOperator = configOperator;  
  24.     }  
  25.       
  26.     //声明两个抽象的执行方法execute()  
  27.     public abstract void execute(String args);  
  28.     public abstract void execute();  
  29. }  
  30.   
  31. //增加命令类:具体命令  
  32. class InsertCommand extends Command {  
  33.     public InsertCommand(String name) {  
  34.         super(name);  
  35.     }  
  36.       
  37.     public void execute(String args) {  
  38.         this.args = args;  
  39.         configOperator.insert(args);  
  40.     }  
  41.       
  42.     public void execute() {  
  43.         configOperator.insert(this.args);  
  44.     }  
  45. }  
  46.   
  47. //修改命令类:具体命令  
  48. class ModifyCommand extends Command {  
  49.     public ModifyCommand(String name) {  
  50.         super(name);  
  51.     }  
  52.       
  53.     public void execute(String args) {  
  54.         this.args = args;  
  55.         configOperator.modify(args);  
  56.     }  
  57.       
  58.     public void execute() {  
  59.         configOperator.modify(this.args);  
  60.     }  
  61. }  
  62.   
  63. //省略了删除命令类DeleteCommand  
  64.   
  65. //配置文件操作类:请求接收者。由于ConfigOperator类的对象是Command的成员对象,它也将随Command对象一起写入文件,因此ConfigOperator也需要实现Serializable接口  
  66. class ConfigOperator implements Serializable {  
  67.     public void insert(String args) {  
  68.         System.out.println("增加新节点:" + args);  
  69.     }  
  70.       
  71.     public void modify(String args) {  
  72.         System.out.println("修改节点:" + args);  
  73.     }  
  74.       
  75.     public void delete(String args) {  
  76.         System.out.println("删除节点:" + args);  
  77.     }  
  78. }  
  79.   
  80. //配置文件设置窗口类:请求发送者  
  81. class ConfigSettingWindow {  
  82.     //定义一个集合来存储每一次操作时的命令对象  
  83.     private ArrayList<Command> commands = new ArrayList<Command>();  
  84.     private Command command;   
  85.   
  86.     //注入具体命令对象  
  87.     public void setCommand(Command command) {  
  88.         this.command = command;  
  89.     }  
  90.       
  91.     //执行配置文件修改命令,同时将命令对象添加到命令集合中  
  92.     public void call(String args) {  
  93.         command.execute(args);  
  94.         commands.add(command);  
  95.     }  
  96.       
  97.     //记录请求日志,生成日志文件,将命令集合写入日志文件  
  98.     public void save() {  
  99.         FileUtil.writeCommands(commands);  
  100.     }  
  101.       
  102.     //从日志文件中提取命令集合,并循环调用每一个命令对象的execute()方法来实现配置文件的重新设置  
  103.     public void recover() {  
  104.         ArrayList list;  
  105.         list = FileUtil.readCommands();  
  106.           
  107.         for (Object obj : list) {  
  108.             ((Command)obj).execute();  
  109.         }  
  110.     }  
  111. }  
  112.   
  113. //工具类:文件操作类  
  114. class FileUtil {  
  115.     //将命令集合写入日志文件  
  116.     public static void writeCommands(ArrayList commands) {  
  117.         try {  
  118.             FileOutputStream file = new FileOutputStream("config.log");  
  119.             //创建对象输出流用于将对象写入到文件中  
  120.             ObjectOutputStream objout = new ObjectOutputStream(new BufferedOutputStream(file));  
  121.             //将对象写入文件  
  122.             objout.writeObject(commands);  
  123.             objout.close();  
  124.             }  
  125.         catch(Exception e) {  
  126.                 System.out.println("命令保存失败!");    
  127.                 e.printStackTrace();  
  128.             }  
  129.     }  
  130.       
  131.     //从日志文件中提取命令集合  
  132.     public static ArrayList readCommands() {  
  133.         try {  
  134.             FileInputStream file = new FileInputStream("config.log");  
  135.             //创建对象输入流用于从文件中读取对象  
  136.             ObjectInputStream objin = new ObjectInputStream(new BufferedInputStream(file));  
  137.               
  138.             //将文件中的对象读出并转换为ArrayList类型  
  139.             ArrayList commands = (ArrayList)objin.readObject();  
  140.             objin.close();  
  141.             return commands;  
  142.             }  
  143.         catch(Exception e) {  
  144.                 System.out.println("命令读取失败!");  
  145.                 e.printStackTrace();  
  146.                 return null;      
  147.             }         
  148.     }  
  149. }  

       编写如下客户端测试代码:

[java]  view plain copy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         ConfigSettingWindow csw = new ConfigSettingWindow(); //定义请求发送者  
  4.         Command command; //定义命令对象  
  5.         ConfigOperator co = new ConfigOperator(); //定义请求接收者  
  6.           
  7.         //四次对配置文件的更改  
  8.         command = new InsertCommand("增加");  
  9.         command.setConfigOperator(co);  
  10.         csw.setCommand(command);  
  11.         csw.call("网站首页");  
  12.           
  13.         command = new InsertCommand("增加");  
  14.         command.setConfigOperator(co);  
  15.         csw.setCommand(command);  
  16.         csw.call("端口号");  
  17.           
  18.         command = new ModifyCommand("修改");  
  19.         command.setConfigOperator(co);  
  20.         csw.setCommand(command);  
  21.         csw.call("网站首页");  
  22.           
  23.         command = new ModifyCommand("修改");  
  24.         command.setConfigOperator(co);  
  25.         csw.setCommand(command);          
  26.         csw.call("端口号");  
  27.           
  28.         System.out.println("----------------------------");  
  29.         System.out.println("保存配置");  
  30.         csw.save();  
  31.               
  32.         System.out.println("----------------------------");   
  33.         System.out.println("恢复配置");  
  34.         System.out.println("----------------------------");   
  35.         csw.recover();    
  36.     }  
  37. }  

       编译并运行程序,输出结果如下:

增加新节点:网站首页

增加新节点:端口号

修改节点:网站首页

修改节点:端口号

----------------------------

保存配置

----------------------------

恢复配置

----------------------------

增加新节点:网站首页

增加新节点:端口号

修改节点:网站首页

修改节点:端口号


7.4宏命令

宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员可以是简单命令,还可以继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理,其结构如图7所示:

7  宏命令结构图

范例

7.1撤销-重做(仅支持单步撤销的计算数字的例子)

类图如下:

源代码如下:

[java]  view plain copy
  1. <span style="white-space:pre">  </span>package _09_Command.example;  
  2.     public abstract class Command {  
  3.         Receiver receiver;  
  4.         int parm;  
  5.   
  6.         public Command(Receiver rec, int parm) {  
  7.             this.receiver = rec;  
  8.             this.parm = parm;  
  9.         }  
  10.   
  11.         public abstract void execute();  
  12.     }  
  13.   
  14.     public class AddCommand extends Command {  
  15.         public AddCommand(Receiver rec, int parm) {  
  16.             super(rec, parm);  
  17.         }  
  18.   
  19.         @Override  
  20.         public void execute() {  
  21.             receiver.action(CMD.ADD, parm);  
  22.         }  
  23.     }  
  24.   
  25.     public class SubCommand extends Command {  
  26.         public SubCommand(Receiver rec, int parm) {  
  27.             super(rec, parm);  
  28.         }  
  29.   
  30.         @Override  
  31.         public void execute() {  
  32.             receiver.action(CMD.SUB, parm);  
  33.         }  
  34.     }  
  35.   
  36.     public class MulCommand extends Command {  
  37.         public MulCommand(Receiver rec, int parm) {  
  38.             super(rec, parm);  
  39.         }  
  40.   
  41.         @Override  
  42.         public void execute() {  
  43.             receiver.action(CMD.MUL, parm);  
  44.         }  
  45.     }  
  46.   
  47.     public class DivCommand extends Command {  
  48.         public DivCommand(Receiver rec, int parm) {  
  49.             super(rec, parm);  
  50.         }  
  51.   
  52.         @Override  
  53.         public void execute() {  
  54.             receiver.action(CMD.DIV, parm);  
  55.         }  
  56.     }  
  57.   
  58.     public class SetCommand extends Command {  
  59.         public SetCommand(Receiver rec, int parm) {  
  60.             super(rec, parm);  
  61.         }  
  62.   
  63.         @Override  
  64.         public void execute() {  
  65.             receiver.action(CMD.SET, parm);  
  66.         }  
  67.     }  
  68.   
  69.     public class UndoCommand extends Command {  
  70.         public UndoCommand(Receiver rec, int parm) {  
  71.             super(rec, 0);  
  72.         }  
  73.   
  74.         @Override  
  75.         public void execute() {  
  76.             receiver.action(CMD.UNDO, 0);  
  77.         }  
  78.     }  
  79.   
  80.     enum CMD {  
  81.         ADD, SUB, MUL, DIV, SET, UNDO  
  82.     };  
  83.   
  84.     public abstract class Receiver {  
  85.         protected int result = 0;  
  86.         protected int lastParm;  
  87.         protected CMD lastAction;  
  88.   
  89.         public abstract void action(CMD cmd, int parm);  
  90.     }  
  91.   
  92.     public class ConcreteReceiver extends Receiver {  
  93.         @Override  
  94.         public void action(CMD cmd, int parm) {  
  95.             switch (cmd) {  
  96.             case ADD:  
  97.                 System.out.print(result + " + " + parm + " = ");  
  98.                 result += parm;  
  99.                 lastParm = parm;  
  100.                 lastAction = CMD.ADD;  
  101.                 System.out.println(result);  
  102.                 System.out  
  103.                         .println("----------------------------------------------");  
  104.                 break;  
  105.             case SUB:  
  106.                 System.out.print(result + " - " + parm + " = ");  
  107.                 result -= parm;  
  108.                 lastParm = parm;  
  109.                 lastAction = CMD.SUB;  
  110.                 System.out.println(result);  
  111.                 System.out  
  112.                         .println("----------------------------------------------");  
  113.                 break;  
  114.             case MUL:  
  115.                 System.out.print(result + " x " + parm + " = ");  
  116.                 result *= parm;  
  117.                 lastParm = parm;  
  118.                 lastAction = CMD.MUL;  
  119.                 System.out.println(result);  
  120.                 System.out  
  121.                         .println("----------------------------------------------");  
  122.                 break;  
  123.             case DIV:  
  124.                 System.out.print(result + " / " + parm + " = ");  
  125.                 result /= parm;  
  126.                 lastParm = parm;  
  127.                 lastAction = CMD.DIV;  
  128.                 System.out.println(result);  
  129.                 System.out  
  130.                         .println("----------------------------------------------");  
  131.                 break;  
  132.             case SET:  
  133.                 result = parm;  
  134.                 lastParm = 0;  
  135.                 lastAction = CMD.SET;  
  136.                 System.out.println("清除并设置值: " + result);  
  137.                 System.out  
  138.                         .println("----------------------------------------------");  
  139.                 break;  
  140.             case UNDO:  
  141.                 if (lastAction != CMD.UNDO && lastAction != CMD.SET) {  
  142.                     switch (lastAction) {  
  143.                     case ADD:  
  144.                         result -= lastParm;  
  145.                         break;  
  146.                     case SUB:  
  147.                         result += lastParm;  
  148.                         break;  
  149.                     case MUL:  
  150.                         result /= lastParm;  
  151.                         break;  
  152.                     case DIV:  
  153.                         result *= lastParm;  
  154.                         break;  
  155.                     }  
  156.                     lastAction = CMD.UNDO;  
  157.                     System.out.println("撤销上步操作后值: " + result);  
  158.                     System.out  
  159.                             .println("----------------------------------------------");  
  160.                 }  
  161.                 break;  
  162.             }  
  163.         }  
  164.     }  
  165.   
  166.     public class Invoker {  
  167.         Command command;  
  168.   
  169.         public void action() {  
  170.             command.execute();  
  171.         }  
  172.   
  173.         public void setCommand(Command cmd) {  
  174.             command = cmd;  
  175.         }  
  176.     }  
  177.   
  178.     public class Client {  
  179.         public static void main(String[] args) {// // just for test  
  180.             Receiver rec = new ConcreteReceiver();  
  181.             Command cmds[] = { new SetCommand(rec, 5), new AddCommand(rec, 5),  
  182.                     new SubCommand(rec, 5), new UndoCommand(rec, 5),  
  183.                     new MulCommand(rec, 5), new DivCommand(rec, 5) };  
  184.             Invoker inv = new Invoker();  
  185.             for (int i = 0; i < 6; i++) {  
  186.                 inv.setCommand(cmds[i]);  
  187.                 inv.action();  
  188.             }  
  189.         }  
  190.     }  

结果如下:

清除并设置值: 5

----------------------------------------------

5 + 5 = 10

----------------------------------------------

10 - 5 = 5

----------------------------------------------

撤销上步操作后值: 10

----------------------------------------------

10 x 5 = 50

----------------------------------------------

50 / 5 = 10

----------------------------------------------

7.2自定义功能键

  Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。Sunny软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图2所示:

2  “功能键设置”界面效果图

       通过如图2所示界面,用户可以将功能键和相应功能绑定在一起,还可以根据需要来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。

       Sunny软件公司某开发人员欲使用如下代码来实现功能键与功能处理类之间的调用关系:

[java]  view plain copy
  1. //FunctionButton:功能键类,请求发送者  
  2. class FunctionButton {  
  3.     private HelpHandler help; //HelpHandler:帮助文档处理类,请求接收者  
  4.       
  5.     //在FunctionButton的onClick()方法中调用HelpHandler的display()方法  
  6. public void onClick() {  
  7.         help = new HelpHandler();  
  8.         help.display(); //显示帮助文档  
  9.     }  
  10. }  

       在上述代码中,功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButtononClick()方法中将调用接收者HelpHandlerdisplay()方法。显然,如果使用上述代码,将给系统带来如下几个问题:

       (1) 由于请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,如果需要将请求接收者HelpHandler改为WindowHanlder(窗口处理类),则需要修改FunctionButton的源代码,违背了“开闭原则”。

       (2) FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。由于请求接收者HelpHandlerWindowHanlder等类之间可能不存在任何关系,它们没有共同的抽象层,因此也很难依据“依赖倒转原则”来设计FunctionButton

       (3) 用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,系统缺乏灵活性。

       不难得知,所有这些问题的产生都是因为请求发送者FunctionButton类和请求接收者HelpHandlerWindowHanlder等类之间存在直接耦合关系,如何降低请求发送者和接收者之间的耦合度,让相同的发送者可以对应不同的接收者?这是Sunny软件公司开发人员在设计“功能键设置”模块时不得不考虑的问题。命令模式正为解决这类问题而诞生,此时,如果我们使用命令模式,可以在一定程度上解决上述问题(注:命令模式无法解决类的个数增加的问题)。

完整解决方案

       为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,Sunny软件公司开发人员使用命令模式来设计“自定义功能键”模块,其核心结构如图4所示:

 

自定义功能键核心结构图

       在图4中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommandHelpCommand充当具体命令类,WindowHanlderHelpHandler充当请求接收者。完整代码如下所示:

[java]  view plain copy
  1. import java.util.*;  
  2.   
  3. //功能键设置窗口类  
  4. class FBSettingWindow {  
  5.     private String title; //窗口标题  
  6.     //定义一个ArrayList来存储所有功能键  
  7.     private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();  
  8.       
  9.     public FBSettingWindow(String title) {  
  10.         this.title = title;  
  11.     }  
  12.       
  13.     public void setTitle(String title) {  
  14.         this.title = title;  
  15.     }  
  16.       
  17.     public String getTitle() {  
  18.         return this.title;  
  19.     }  
  20.       
  21.     public void addFunctionButton(FunctionButton fb) {  
  22.         functionButtons.add(fb);  
  23.     }  
  24.       
  25.     public void removeFunctionButton(FunctionButton fb) {  
  26.         functionButtons.remove(fb);  
  27.     }  
  28.       
  29.     //显示窗口及功能键  
  30.     public void display() {  
  31.         System.out.println("显示窗口:" + this.title);  
  32.         System.out.println("显示功能键:");  
  33.         for (Object obj : functionButtons) {  
  34.             System.out.println(((FunctionButton)obj).getName());  
  35.         }  
  36.         System.out.println("------------------------------");  
  37.     }     
  38. }  
  39.   
  40. //功能键类:请求发送者  
  41. class FunctionButton {  
  42.     private String name; //功能键名称  
  43.     private Command command; //维持一个抽象命令对象的引用  
  44.       
  45.     public FunctionButton(String name) {  
  46.         this.name = name;  
  47.     }  
  48.       
  49.     public String getName() {  
  50.         return this.name;  
  51.     }  
  52.       
  53.     //为功能键注入命令  
  54.     public void setCommand(Command command) {  
  55.         this.command = command;  
  56.     }  
  57.       
  58.     //发送请求的方法  
  59.     public void onClick() {  
  60.         System.out.print("点击功能键:");  
  61.         command.execute();  
  62.     }  
  63. }  
  64.   
  65. //抽象命令类  
  66. abstract class Command {  
  67.     public abstract void execute();  
  68. }  
  69.   
  70. //帮助命令类:具体命令类  
  71. class HelpCommand extends Command {  
  72.     private HelpHandler hhObj; //维持对请求接收者的引用  
  73.       
  74.     public HelpCommand() {  
  75.         hhObj = new HelpHandler();  
  76.     }  
  77.       
  78.     //命令执行方法,将调用请求接收者的业务方法  
  79.     public void execute() {  
  80.         hhObj.display();  
  81.     }  
  82. }  
  83.   
  84. //最小化命令类:具体命令类  
  85. class MinimizeCommand extends Command {  
  86.     private WindowHanlder whObj; //维持对请求接收者的引用  
  87.       
  88.     public MinimizeCommand() {  
  89.         whObj = new WindowHanlder();  
  90.     }  
  91.       
  92. //命令执行方法,将调用请求接收者的业务方法  
  93.     public void execute() {  
  94.         whObj.minimize();  
  95.     }  
  96. }  
  97.   
  98. //窗口处理类:请求接收者  
  99. class WindowHanlder {  
  100.     public void minimize() {  
  101.         System.out.println("将窗口最小化至托盘!");  
  102.     }  
  103. }  
  104.   
  105. //帮助文档处理类:请求接收者  
  106. class HelpHandler {  
  107.     public void display() {  
  108.         System.out.println("显示帮助文档!");  
  109.     }  
  110. }  

       为了提高系统的灵活性和可扩展性,我们将具体命令类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示:

[java]  view plain copy
  1. import javax.xml.parsers.*;  
  2. import org.w3c.dom.*;  
  3. import org.xml.sax.SAXException;  
  4. import java.io.*;  
  5.   
  6. public class XMLUtil {  
  7. //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象,可以通过参数的不同返回不同类名节点所对应的实例  
  8.     public static Object getBean(int i) {  
  9.         try {  
  10.             //创建文档对象  
  11.             DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();  
  12.             DocumentBuilder builder = dFactory.newDocumentBuilder();  
  13.             Document doc;                             
  14.             doc = builder.parse(new File("config.xml"));   
  15.           
  16.             //获取包含类名的文本节点  
  17.             NodeList nl = doc.getElementsByTagName("className");  
  18.             Node classNode = null;  
  19.             if (0 == i) {  
  20.                 classNode = nl.item(0).getFirstChild();  
  21.             }  
  22.             else {  
  23.                 classNode = nl.item(1).getFirstChild();  
  24.             }   
  25.   
  26.             String cName = classNode.getNodeValue();  
  27.               
  28.             //通过类名生成实例对象并将其返回  
  29.             Class c = Class.forName(cName);  
  30.             Object obj = c.newInstance();  
  31.             return obj;  
  32.         }     
  33.         catch(Exception e){  
  34.             e.printStackTrace();  
  35.             return null;  
  36.         }  
  37.     }  
  38. }  

       配置文件config.xml中存储了具体建造者类的类名,代码如下所示:

[java]  view plain copy
  1. <?xml version="1.0"?>  
  2. <config>  
  3.     <className>HelpCommand</className>  
  4.     <className>MinimizeCommand</className>  
  5. </config>  

       编写如下客户端测试代码:

[java]  view plain copy
  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         FBSettingWindow fbsw = new FBSettingWindow("功能键设置");  
  4.               
  5.         FunctionButton fb1,fb2;  
  6.         fb1 = new FunctionButton("功能键1");  
  7.         fb2 = new FunctionButton("功能键1");  
  8.           
  9.         Command command1,command2;  
  10.         //通过读取配置文件和反射生成具体命令对象  
  11.         command1 = (Command)XMLUtil.getBean(0);  
  12.         command2 = (Command)XMLUtil.getBean(1);  
  13.           
  14.         //将命令对象注入功能键  
  15.         fb1.setCommand(command1);  
  16.         fb2.setCommand(command2);  
  17.           
  18.         fbsw.addFunctionButton(fb1);  
  19.         fbsw.addFunctionButton(fb2);  
  20.         fbsw.display();  
  21.           
  22.         //调用功能键的业务方法  
  23.         fb1.onClick();  
  24.         fb2.onClick();  
  25.     }  
  26. }  

       编译并运行程序,输出结果如下:

显示窗口:功能键设置

显示功能键:

功能键1

功能键1

------------------------------

点击功能键:显示帮助文档!

点击功能键:将窗口最小化至托盘!

       如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Java中,可以使用Memento模式来实现撤销和修改功能。Memento模式通过将操作过程中的状态和状态变化保存到备忘录对象中,从而实现撤销功能。另外,通过将备忘录对象的状态恢复到某个指定的状态,可以实现修改功能。 ### 回答2: 在Java中,可以使用command设计模式来实现撤销和修改功能。command设计模式是一种行为设计模式,它将请求封装成一个对象,从而使不同的请求可以被封装成具体的命令类。在实现撤销和修改功能时,可以按照以下步骤进行: 首先,需要定义一个抽象命令类(Command),该类包含一个执行操作的方法execute()。然后,具体的命令类(ConcreteCommand)继承抽象命令类,并实现具体的操作逻辑。例如,创建一个撤销命令类(UndoCommand)和一个修改命令类(ModifyCommand)。 接下来,需要定义一个命令调用者类(CommandInvoker),该类包含一个命令对象,并提供调用方法invoke()来执行命令。该类中还需要一个备份对象(Memento),用于保存当前状态。 在具体业务逻辑中,当需要进行撤销操作时,调用者类(CommandInvoker)可以调用命令对象(ConcreteCommand)的撤销方法(undo()),该方法内部会调用备份对象(Memento)的恢复方法(restore()),从而实现状态的撤销。 同样地,当需要进行修改操作时,调用者类(CommandInvoker)可以调用命令对象(ConcreteCommand)的修改方法(modify()),该方法内部会调用备份对象(Memento)的保存方法(save()),从而保存当前状态。然后,再根据具体的逻辑进行修改操作。 这样,通过command设计模式,在Java中可以方便地实现撤销和修改功能。使用memento设计模式可以很好地将状态进行保存和恢复,而command设计模式则实现了对操作的封装和调用,使得整个过程更加灵活和可控。 ### 回答3: 在Java中,可以利用Command设计模式来实现撤销和修改功能。Command设计模式是一种行为设计模式,它将请求封装成一个对象,从而使不同的请求拥有不同的行为。 首先,我们需要定义一个Command接口,其中包含执行(execute)和撤销(undo)方法。这个接口可以有多个实现类,每个实现类代表一种请求。 接下来,我们可以创建一个具体的命令类,该类实现了Command接口,并且持有一个执行者对象。该类的execute方法就是调用执行者的相应方法来处理请求,undo方法则是调用执行者的相应方法来撤销之前的操作。 然后,我们可以创建一个调用者类,其包含一个命令对象的引用。该调用者类有一个可以执行命令的方法,这个方法会调用命令对象的execute方法。此外,调用者类还可以有一个可以撤销命令的方法,这个方法会调用命令对象的undo方法。 最后,我们可以创建一个客户端类,在这个类中通过创建命令对象、调用者对象和执行者对象,然后将它们组合起来使用。客户端类可以调用调用者对象的执行方法来执行命令,也可以调用调用者对象的撤销方法来撤销命令。 使用Memento设计模式实现撤销和修改功能类似,只是在命令类中多了一个备忘录对象,该对象用于保存命令执行前的状态。在undo方法中,可以将备忘录对象恢复到之前的状态,从而实现撤销操作。 总之,利用Command设计模式和Memento设计模式可以很方便地实现撤销和修改功能,通过封装命令和备忘录对象,可以实现对操作的统一管理和控制,提高代码的可维护性和可复用性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值