设计模式学习笔记(十七):命令模式
1 概述
1.1 引言
日常生活中,可以通过开关控制一些电器的开启和关闭,比如电灯和排气扇。可以将开关理解成一个请求发送者,电灯是请求的最红接收者以及处理者,开关与电灯之间不存在直接的耦合关系,两者通过电线连接在一起,使不同的电线可以连接不同的请求接收者,只需要更换一根电线,相同的发送者(开关)既可对应不同的接收者(电器)。
软件开发中经常需要向某些对象发送请求,但是并不知道具体的接收者是谁,也不知道被请求的操作是哪个,此时希望以一种松耦合的方式来设计软件,使得请求发送者与请求接收者之间能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作,此时可以使用命令模式进行设计。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
1.2 定义
命令模式:将一个请求封装成一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式是一种对象行为型模式,别名为动作模式或者事务模式。
1.3 结构图
1.4 角色
Command
(抽象命令类):抽象命令类一般是一个抽象类或者接口,在其中声明了用于执行请求的execute()
方法,通过这些方法可以调用请求接收者的相关操作ConcreteCommand
(具体命令类):实现了抽象命令类中声明的方法,对应具体的接收者对象,将接收者对象的动作绑定其中,在实现execute()
方法时,将调用接收者对象的相关操作Invoker
(调用者):调用者即请求发送者,通过命令对象来执行请求。一个调用者并不需要设计时确定接收者,因此它只与抽象命令类之间存在关联关系。程序运行时将具体命令对象注入,并调用其中的execute()
方法,从而实现间接调用请求接收者的相关操作Receiver
(接收者):接收者执行与请求相关的操作,具体实现对请求的业务处理
2 典型实现
2.1 步骤
- 定义抽象命令类:定义执行请求的方法
- 定义调用者:在调用方法里面包含对具体命令的调用,同时需要包含一个对抽象命令的引用
- 定义接收者:定义接收请求的业务方法
- 定义具体命令类:继承/实现抽象命令类,实现其中执行请求方法,转发到接收者的接收方法
2.2 抽象命令类
这里实现为一个接口:
interface Command
{
void execute();
}
2.3 调用者
class Invoker
{
private Command command;
public Invoker(Command command)
{
this.command = command;
}
public void call()
{
System.out.println("调用者操作");
command.execute();
}
}
调用者可以通过构造方法或者setter注入具体命令,对外提供一个调用方法call
,当调用此方法时调用具体命令的execute
。
2.4 接收者
class Receiver
{
public void action()
{
System.out.println("接收者操作");
}
}
这里的接收者只有一个action
,表示接收方法。
2.5 具体命令类
class ConcreteCommand implements Command
{
private Receiver receiver = new Receiver();
@Override
public void execute()
{
receiver.action();
}
}
具体命令类中需要包含一个对接收者的引用,以便在execute
中调用接收者。
2.6 客户端
public static void main(String[] args)
{
Invoker invoker = new Invoker(new ConcreteCommand());
invoker.call();
}
通过构造方法注入具体命令到调用者中,接着直接调用即可。
输出如下:
3 实例
自定义功能键的设置,对于一个按钮,可以根据需要由用户设置为最小化/最大化/关闭功能,使用命令模式进行设计。
设计如下:
- 抽象命令类:
Command
- 调用者:
Button
- 接收者:
MinimizeHandler
+MaximizeHandler
+CloseHandler
- 具体命令类:
MinimizeCommand
+MaximizeCommand
+CloseCommand
首先设计抽象命令类,实现为一个接口,仅包含execute
方法:
interface Command
{
void execute();
}
接着是调用者类,包含一个抽象命令的引用:
class Button
{
private Command command;
public Button(Command command)
{
this.command = command;
}
public void onClick()
{
System.out.println("按钮被点击");
command.execute();
}
}
然后是接收者类:
class MinimizeHandler
{
public void handle()
{
System.out.println("最小化");
}
}
class MaximizeHandler
{
public void handle()
{
System.out.println("最大化");
}
}
class CloseHandler
{
public void handle()
{
System.out.println("关闭");
}
}
最后是具体命令类,对应包含一个接收者成员即可,实现其中的execute
并转发到接收者的方法:
class MinimizeCommand implements Command
{
private MinimizeHandler handler = new MinimizeHandler();
@Override
public void execute()
{
handler.handle();
}
}
class MaximizeCommand implements Command
{
private MaximizeHandler handler = new MaximizeHandler();
@Override
public void execute()
{
handler.handle();
}
}
class CloseCommand implements Command
{
private CloseHandler handler = new CloseHandler();
@Override
public void execute()
{
handler.handle();
}
}
测试类:
public static void main(String[] args)
{
Button button = new Button(new MinimizeCommand());
button.onClick();
button = new Button(new MaximizeCommand());
button.onClick();
button = new Button(new CloseCommand());
button.onClick();
}
输出:
如果需要新增一个命令,只需要命令接收者以及实现了Command
的具体命令类,客户端再将具体命令注入请求发送者(Button
),无须直接操作请求接收者。
4 命令队列
有时候需要将多个请求排队,当一个请求发送者发送完成一个请求后,不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法完成对请求的处理。这种形式可以通过命令队列实现,实现命令队列很简单,一般是增加一个叫CommandQueue
的类,由该类负责存储多个命令对象,不同的命令对象可以对应不同的请求接收者,比如在上面的例子中增加CommandQueue
命令队列类:
class CommandQueue
{
private ArrayList<Command> commands = new ArrayList<>();
public void add(Command command)
{
commands.add(command);
}
public void remove(Command command)
{
commands.remove(command);
}
public void execute()
{
System.out.println("批量执行命令");
commands.forEach(Command::execute);
}
}
接着修改调用者类Button
(只需将原来的Command
改为CommandQueue
):
class Button
{
private CommandQueue queue;
public Button(CommandQueue queue)
{
this.queue = queue;
}
public void onClick()
{
System.out.println("按钮被点击");
queue.execute();
}
}
最后是客户端定义命令队列并作为参数传入调用者的构造方法或者setter中,最后由调用者执行方法:
public static void main(String[] args)
{
CommandQueue queue = new CommandQueue();
queue.add(new MinimizeCommand());
queue.add(new MaximizeCommand());
queue.add(new CloseCommand());
Button button = new Button(queue);
button.onClick();
}
输出如下:
5 撤销与重做
设计一个简易计算器,实现加法功能,还能够实现撤销以及重做功能,使用命令模式实现。
设计如下:
- 抽象命令类:
Command
- 调用者:
Calculator
- 接收者:
Adder
- 具体命令类:
AddCommand
首先先不实现撤销以及重做功能:
public class Test
{
public static void main(String[] args)
{
Calculator calculator = new Calculator(new AddCommand());
calculator.add(3);
calculator.add(9);
}
}
interface Command
{
int execute(int value);
}
class Calculator
{
private Command command;
public Calculator(Command command)
{
this.command = command;
}
public void add(int value)
{
System.out.println(command.execute(value));
}
}
class Adder
{
private int num = 0;
public int add(int value)
{
return num += value;
}
}
class AddCommand implements Command
{
private Adder adder = new Adder();
@Override
public int execute(int value)
{
return adder.add(value);
}
}
代码与上面的实例类似,就不解释了。
这里关键的问题是如何实现撤销以及重做功能,撤销能够恢复到进行加法之前的状态,而重做能恢复到进行了加法之后的状态,而且这是有固定顺序的,因此可以联想到数组,使用下标表示当前状态,下标左移表示撤销,下标右移表示重做:
使用一个状态数组存储每次进行加法的状态,用下标表示当前状态,当撤销时,使下标左移,当重做时,使下标右移。
首先需要修改抽象命令类,添加撤销以及重做方法:
interface Command
{
int execute(int value);
int undo();
int redo();
}
接着修改调用者类,添加撤销以及重做方法:
class Calculator
{
private Command command;
public Calculator(Command command)
{
this.command = command;
}
public void add(int value)
{
System.out.println(command.execute(value));
}
public void undo()
{
System.out.println(command.undo());
}
public void redo()
{
System.out.println(command.redo());
}
}
核心的实现位于接收者类Adder
,使用了List<Integer>
存储了状态,index
表示下标,在撤销或重做之前首先判断下标位置是否合法,合法则进行下一步操作:
class Adder
{
private List<Integer> nums = new ArrayList<>();
private int index = 0;
public Adder()
{
nums.add(0);
}
public int add(int value)
{
int result = nums.get(index)+value;
nums.add(result);
++index;
return result;
}
public int redo()
{
if(index + 1 < nums.size())
return nums.get(++index);
return nums.get(index);
}
public int undo()
{
if(index - 1 >= 0)
return nums.get(--index);
return nums.get(index);
}
}
最后具体命令类简单添加撤销以及重做方法即可:
class AddCommand implements Command
{
private Adder adder = new Adder();
@Override
public int execute(int value)
{
return adder.add(value);
}
@Override
public int undo()
{
return adder.undo();
}
@Override
public int redo()
{
return adder.redo();
}
}
测试:
public static void main(String[] args)
{
Calculator calculator = new Calculator(new AddCommand());
calculator.add(3);
calculator.add(9);
calculator.undo();
calculator.undo();
calculator.undo();
calculator.undo();
calculator.redo();
calculator.redo();
calculator.redo();
calculator.redo();
}
6 主要优点
- 降低耦合度:由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求可以对应不同的接收者,同样相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性
- 满足OCP:新的命令可以很容易添加到系统中,由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,满足OCP的要求
- 撤销+中作:为请求的撤销以及重做提供了一种设计和实现方案
7 主要缺点
- 过多具体命令类:使用命令模式可能会导致系统有过多的具体命令类,因为针对每一个请求接收者的调用操作都需要设计一个具体工具类,因此在某些系统中可能需要提供大量的具体命令类
8 适用场景
- 系统需要将请求调用者和请求接受者解耦,使得调用者和接收者不直接交互,请求调用者无须知道接收者的存在,也无需知道接收者是谁,接收者也无须关心何时被调用
- 系统需要在不同时间指定请求,将请求排队和执行请求
- 系统需要支持撤销以及恢复操作
- 系统需要将一组操作组合一起形成宏命令,使用命令队列实现