设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式分为三种类型,共23种。
创建型模式(5):单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式(7):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式(11):(父子类)策略模式、模版方法模式,(两个类)观察者模式、迭代器模式、职责链模式、命令模式,(类的状态)状态模式、备忘录模式,(中间类) 访问者模式、中介者模式、解释器模式。
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式(Command Pattern)。
1)顾客与厨师
假设有一个快餐店,而我是该快餐店的点餐服务员,那么我一天的工作应该是这样的:当某位客人点餐或者打来订餐电话后,我会把他的需求都写在清单上,然后交给厨房,客人不用关心的是哪些厨师帮他炒菜。我们餐厅还可以满足客人需要的定时服务,比如客人可能当前正在回家的路上,要求一个小时后才开始炒他的菜,只要订单还在,厨师就不会忘记。客人也可以很方便地打电话来撤销订单。另外如果有太多的客人点餐,厨房可以按照订单的顺序排队炒菜。
这些记录着订餐信息的清单,便是命令模式中的命令对象。
客人需要向厨师发送请求,但是完全不知道这些厨师的名字和联系方式,也不知道厨师炒菜的方式和步骤。命令模式把客人订餐的请求封装成command对象,也就是订餐中的订单对象。这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师手中。这样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。
另外,相对于过程化的请求调用,command对象拥有更长的生命周期。对象的生命周期是跟初始请求无关的,因为这个请求已经被封装在了command对象的方法中,成为了这个对象的行为。我们可以在程序运行的任意时刻去调用这个方法,就像厨师可以在客人预订一个小时之后才帮他炒菜,相当于程序在一个小时后才开始执行command对象的方法。除了这两点之外,命令模式还支持撤销,排队等操作。
2)开关与电器
装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或者排气扇。在购买开关时,我们并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。开关与电器之间通过电线建立连接,如果开关打开,则电线通电,电器工作;反之,开关关闭,电线断电,电器停止工作。相同的开关可以通过不同的电线来控制不同的电器,如图所示:
在图1中,我们可以将开关理解成一个请求的发送者,用户通过它来发送一个“开灯”请求,而电灯是“开灯”请求的最终接收者和处理者,在图中,开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者,只需更换一根电线,相同的发送者(开关)即可对应不同的接收者(电器)。
在软件开发中也存在很多与开关和电器类似的请求发送者和接收者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接收者。为了降低系统的耦合度,将请求的发送者和接收者解耦,我们可以使用一种被称之为命令模式的设计模式来设计系统,在命令模式中,发送者与接收者之间引入了新的命令对象(类似图1中的电线),将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。本章我们将学习用于将请求发送者和接收者解耦的命令模式。
Command Pattern(命令模式):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
Command Pattern:Encapsulate a request as an object,thereby letting you parameterize clients with different requests,queue or log requests, and support undoable operations.
命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如图所示:
在命令模式结构图中包含如下几个角色:
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。典型的代码如下:
抽象命令:
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() {
//具体操作
}
}
Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。Sunny软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图所示:
完整代码如下:
import java.util.ArrayList;
public class CommandClient {
public static void main(String[] args) {
FBSettingWindow fbsw = new FBSettingWindow("功能键设置");
FunctionButton fb1,fb2;
fb1 = new FunctionButton("功能键1");
fb2 = new FunctionButton("功能键2");
Command command1,command2;
//通过读取配置文件和反射生成具体命令对象
command1 = new HelpCommand();
command2 = new MinimizeCommand();
//将命令对象注入功能键,实现命令对象参数化
fb1.setCommand(command1);
fb2.setCommand(command2);
fbsw.addFunctionButton(fb1);
fbsw.addFunctionButton(fb2);
fbsw.show();
//调用功能键的业务方法
fb1.onClick();
fb2.onClick();
}
}
// 功能键设置窗口类
class FBSettingWindow {
private String title; // 窗口标题
// 定义一个ArrayList来存储所有功能键
private ArrayList functionButtons =
new ArrayList();
public FBSettingWindow(String title) {
this.title = title;
}
public void addFunctionButton(FunctionButton fb) {
functionButtons.add(fb);
}
public void removeFunctionButton(FunctionButton fb) {
functionButtons.remove(fb);
}
// 显示窗口及功能键
public void show() {
System.out.println("显示窗口:" + this.title);
System.out.println("显示功能键:");
for (FunctionButton obj : functionButtons) {
System.out.println(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("点击"+name+":");
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("显示帮助文档!");
}
}
输出结果如下:
显示窗口:功能键设置
显示功能键:
功能键1
功能键2
------------------------------
点击功能键1:显示帮助文档!
点击功能键2:将窗口最小化至托盘!
每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:
public class CommandQueue {
//定义一个ArrayList来存储命令队列
private ArrayList commands = new ArrayList();
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循环调用每一个命令对象的execute()方法
public void execute() {
for (Command command : commands) {
command.execute();
}
}
}
在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下:
public 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()方法,从而提高程序的执行效率。
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。
拓展:除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。
Sunny软件公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。
Sunny软件公司开发人员使用命令模式设计了如图所示结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。
public class CalcClient {
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();
//form.undo(); 如何实现多次撤销
}
}
// 加法类:请求接收者
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);
}
}
需要注意的是在本实例中只能实现一步撤销操作,因为没有保存命令对象的历史状态,可以通过引入一个命令集合或其他方式来存储每一次操作时命令的状态,从而实现多次撤销操作。除了Undo操作外,还可以采用类似的方式实现恢复(Redo)操作,即恢复所撤销的操作(或称为二次撤销)。
思考:修改简易计算器源代码,使之能够实现多次撤销(Undo)和恢复(Redo)。
请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。很多系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以记录用户对系统的一些操作(例如对数据的更改)。请求日志文件可以实现很多功能,常用功能如下:
(1) “天有不测风云”,一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一个特定的状态;
(2) 请求日志也可以用于实现批处理,在一个请求日志文件中可以存储一系列命令对象,例如一个命令队列;
(3) 可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。
在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现Java.io.Serializable接口。下面我们通过一个简单实例来说明日志文件的用途以及如何实现请求日志:
现在Sunny软件公司开发人员希望将对配置文件的操作请求记录在日志文件中,如果网站重新部署,只需要执行保存在日志文件中的命令对象即可修改配置文件。
package command;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
public class LogClient {
public static void main(String args[]) {
ConfigSettingWindow csw = new ConfigSettingWindow(); // 定义请求发送者
LogCommand 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();
}
}
// 抽象命令类,由于需要将命令对象写入文件,因此它实现了Serializable接口
abstract class LogCommand implements Serializable {
protected String name; // 命令名称
protected String args; // 命令参数
protected ConfigOperator configOperator; // 维持对接收者对象的引用
public LogCommand(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 LogCommand {
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 LogCommand {
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 {
private static final long serialVersionUID = 1L;
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 commands = new ArrayList();
private LogCommand command;
// 注入具体命令对象
public void setCommand(LogCommand 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 (LogCommand obj : list) {
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;
}
}
}
输出结果:
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
----------------------------
保存配置
----------------------------
恢复配置
----------------------------
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员可以是简单命令,还可以继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。
优点:
缺点:
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
使用环境:
本文大多内容都来自这篇博文:JAVA设计模式(15):行为型-命令模式(Command)
博文参考: 设计模式 —— 命令模式(Command Pattern)
博文参考:命令模式
参考电子书下载:设计模式的艺术–软件开发人员内功修炼之道_刘伟(2013年).pdf
《道德经》第九章:
持而盈之,不如其已;揣而锐之,不可长保。金玉满堂,莫之能守;富贵而骄,自遗其咎。功成身退,天之道也。
译文:执持盈满,不如适时停止;显露锋芒,锐势难以保持长久。金玉满堂,无法守藏;如果富贵到了骄横的程度,那是自己留下了祸根。一件事情做的圆满了,就要含藏收敛,这是符合自然规律的道理。