本文实例代码:https://github.com/JamesZBL/java_design_patterns
命令(Command)模式是一种数据驱动的设计模式,它属于行为型模式。请求被包装成一个命令对象,并由调用者传递给被调用对象。被调用对象寻找可以处理该命令的合适的处理对象,并把该命令传给这个处理对象,该处理对象执行命令。
命令模式中,命令的发出者和接收者是独立的,发出命令的职责和处理命令的职责被分别指派给不同的对象。命令模式解决了一般的调用过程中,“行为请求者”与“行为实现者” 之间的强耦合关系。比如某些场合中,需要对命令进行“撤销”、“重做”,亦或其他不得不以 “事务” 的形式实现的场合,命令发出者和命令实现者之间的解耦就显得至关重要了。
实例
命令模式最常见的场景就是字处理软件了,软件必须允许使用者进行重做或撤销的操作,仿佛没有这种特性的字处理软件几乎不会有人乐意使用。
首先不考虑使用命令模式,而是以传统的对象间调用来实现这种需求。假设我们现在需要对某个字依次进行如下操作:增大字号、设置字体颜色为红色、设置为加粗,那么对于命令发出者,要处理的逻辑就是这样的:
if(改变字号){
if(增大字号){
字号渲染器.增大字号();
}else if(减小字号){
字号渲染器.减小字号();
}
}
if(改变颜色){
颜色渲染器.改变颜色(颜色);
}
if(改变粗细){
if(加粗){
粗细渲染器.加粗();
}else if(不加粗){
粗细渲染器.变细();
}
}
这种 “紧耦合” 的结构下,行为请求者与行为实现者之间的关系大概是这样的:
首先,所有命令都要有对应的处理者去执行,这就意味着行为请求者需要持有多个处理者的引用。这样,每修改或添加一个命令就必须修改行为请求者的逻辑,没办法实现对扩展开放。其次,如果要实现对命令的撤销或重做,那么本来就复杂的行为判断逻辑会变得愈发臃肿,对程序员来说这简直是一场灾难。
那么将行为请求者和行为实现者进行解耦后是怎样的呢?大概如图:
将行为请求者和行为实现者解耦,直观的改变就是:行为请求者不会直接调用行为实现者的具体方法,而是向行为实现者发出包含行为的具体命令,这个命令通常以对象的形式出现,他们之间传递的消息就是命令模式中的核心元素 —— “命令”,他在原始的行为请求者和行为实现者之间架起了一条高速公路,原来臃肿的逻辑判断代码得到了缩减,逻辑判断的职责被指派给了行为实现者,而行为请求者只需专注于发出正确的命令。
为了将使行为请求者和行为的具体实现解耦,应当将命令进行抽象。继续上面字处理软件的例子,用户发出的命令应当至少包含 “撤销” 和 “重做” 的功能,因此我们可以这样设计 “命令” 抽象类:
Command.java
public abstract class Command {
/**
* 执行
*/
public abstract void execute();
/**
* 撤销
*/
public abstract void undo();
/**
* 重做
*/
public abstract void redo();
}
增大字体的命令:
Enlarge.java
public class Enlarge extends Command {
private final AbstractFont font;
private final Size oriSize;
public Enlarge(AbstractFont font) {
this.font = font;
oriSize = font.getSize();
}
@Override
public void execute() {
font.setSize(Size.LARGE);
}
@Override
public void undo() {
font.setSize(oriSize);
}
@Override
public void redo() {
execute();
}
}
使字体变红:
Rubify.java
public class Rubify extends Command {
private final AbstractFont font;
private final Color oriColor;
public Rubify(AbstractFont font) {
this.font = font;
oriColor = font.getColor();
}
@Override
public void execute() {
font.setColor(Color.RED);
}
@Override
public void undo() {
font.setColor(oriColor);
}
@Override
public void redo() {
execute();
}
}
这两种命令的共同点是都继承自命令抽象类,并持有一个行为实施对象的引用,也就是说,行为的具体实现过程被封装到了每个具体的命令中。
那么现在就需要一个行为请求者来发出命令了:
Typist.java
public final class Typist {
private static final Logger LOGGER = LoggerFactory.getLogger(Typist.class);
private Deque<Command> redoStack = new LinkedList<>();
private Deque<Command> undoStack = new LinkedList<>();
public void cast(Command command, AbstractFont font) {
LOGGER.info("{}正在处理字体,命令为:{},处理的字体为:{}", this, command, font);
command.execute();
undoStack.offerLast(command);
}
public void undo() {
if (!undoStack.isEmpty()) {
Command previousCommand = undoStack.pollLast();
redoStack.offerLast(previousCommand);
LOGGER.info("{}正在进行撤销操作,命令为:{}", this, previousCommand);
previousCommand.undo();
} else {
LOGGER.info("没有可以撤销的操作了");
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command previousCommand = redoStack.pollLast();
undoStack.offerLast(previousCommand);
LOGGER.info("{}正在进行重做操作,命令为:{}", this, previousCommand);
previousCommand.redo();
} else {
LOGGER.info("没有可以重做的操作了");
}
}
}
我们分别用两个队列来记录执行过的命令与撤销过的命令,“撤销” 操作对应 “已执行” 的操作, “重做” 操作对应 “已撤销” 的操作。每次执行命令,同时把命令放到 “撤销” 队列的队尾;同样的,每次执行 “撤销” 操作,也同时将命令放到 “重做” 队列的队尾。
为了方便 “观察” 字体的状态,我们将字体抽象成一个类:
AbstractFont.java
public abstract class AbstractFont {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractFont.class);
private Size size;
private Color color;
public Size getSize() {
return size;
}
public void setSize(Size size) {
this.size = size;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
@Override
public abstract String toString();
/**
* 打印当前状态
*/
public void printStatus() {
LOGGER.info("字体当前状态为:\t字体大小:{}\t颜色:{}", getSize(), getColor());
}
}
拉以为用户来试试这个字处理软件吧,我们在旁边观察字体的状态:
App.java
public class Application {
public static void main(String[] args) {
Typist sizeTypist = new Typist();
Typist colorTypist = new Typist();
RegularScript font = new RegularScript();
Command rubify = new Rubify(font);
Command enlarge = new Enlarge(font);
font.printStatus();
// 设置字体颜色
colorTypist.cast(rubify, font);
font.printStatus();
// 设置字体大小
sizeTypist.cast(enlarge, font);
font.printStatus();
// 撤销颜色更改
colorTypist.undo();
font.printStatus();
colorTypist.undo();
// 撤销大小更改
sizeTypist.undo();
font.printStatus();
sizeTypist.undo();
// 字体颜色重做
colorTypist.redo();
font.printStatus();
colorTypist.redo();
// 大小重做
sizeTypist.redo();
font.printStatus();
sizeTypist.redo();
}
}
观察到的结果:
小字,黑色
小字,红色
大字,红色
大字,黑色
小字,黑色
大字,黑色
大字,红色
总结
命令模式的主要适用场景有:
需要将行为请求者和行为实现者解耦,使得请求者和行为作用对象不直接交互
需要在不同的时间发出请求、使请求排队以及执行队列中的请求
需要支持命令的撤销操作和重做等类似操作
需要将一组操作组合在一起,即实现宏命令
个人博客同步更新,获取更多技术分享请关注:郑保乐的博客