设计模式完结(14)-- 命令模式 --- 请求发送者与接收者解耦
定义:把一个请求或者操作封装在命令对象中。命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
Invoker类 被客户端调用,可以接受命令请求,设计命令队列,决定是否相应该请求,记录或撤销或重做命令请求,记录日志等等.
命令模式 --- 请求发送者与接收者解耦
解决了这种耦合的好处我认为主要有两点:
1.更方便的对命令进行扩展(注意:这不是主要的优势,后面会提到)
2.对多个命令的统一控制(这种控制包括但不限于:队列、撤销/恢复、记录日志等等)
命令模式概述
在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
命令模式定义如下:
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
起连接作用: 类似开关 和 电器 之间的 电线
请求发送者与接收者解耦, 引入命令类
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 ConcreteCommand extends Command { private Receiver receiver; //维持一个对请求接收者对象的引用 public void execute() { receiver.action(); //调用请求接收者的业务处理方法action() } } class Receiver { public void action() { //具体操作 } }
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> 编写如下客户端测试代码: [java] view plain copy 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 ------------------------------ 点击功能键:显示帮助文档! 点击功能键:将窗口最小化至托盘!
命令队列的实现:
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(); } } } 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()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。
请求日志:
在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现Java.io.Serializable接口。下面我们通过一个简单实例来说明日志文件的用途以及如何实现请求日志:
Sunny软件公司开发了一个网站配置文件管理工具,可以通过一个可视化界面对网站配置文件进行增删改等操作,该工具使用命令模式进行设计,结构如图6所示:
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(); } } 编译并运行程序,输出结果如下: 增加新节点:网站首页 增加新节点:端口号 修改节点:网站首页 修改节点:端口号 ---------------------------- 保存配置 ---------------------------- 恢复配置 ---------------------------- 增加新节点:网站首页 增加新节点:端口号 修改节点:网站首页 修改节点:端口号
宏命令:
宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。
- 适用场景
在以下情况下可以考虑使用命令模式:
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(4) 系统需要将一组操作组合在一起形成宏命令。