命令模式(行为型模式)
模式概述
命令模式是一种行为设计模式,它可将请求转换为一个包含所有上下文信息的独立对象,并根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能撤销操作。
问题
假如李星云正在开发一款新游戏,当前的任务是创建一个包含多个功能的工具栏,并让每个按钮对应人物的不同行为、动作或道具。
李星云创建了一个非常简洁的按钮类,它不仅可用于生成工具栏中的按钮,还可用于生成各种对话框的通用按钮。
所有按钮看上去都很相似,但它们可以完成不同的操作,如打开行囊,整理行囊,装备道具,使用道具等。
在哪里放置这些按钮的处理代码合适呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类或方法,这些子类或方法中包含点击后必须执行的代码。

很快,他就意识到这种方式有问题。
因为创建了大量的子类,所以每当对父类有任何的修改,都有可能需要修改所有子类的代码。
某个功能可能会在多个地方被调用,例如人物移动,可以点击工具栏中的“前行”按钮,或者直接使用键盘上的“w”键。这样的话,会迅速制造有大量重复性代码。
方案
优秀的软件设计通常会将关注点进行分离,而这往往会导致软件的分层。例如MVC模式:一层负责模型,一层负责控住转发,而另一层则负责界面展现。

命令模式建议不要直接这样操作,而是应该将所有的细节抽取出来,封装到单独的命令类中,该类仅包含一个用于触发请求的方法。

下一步是让所有命令都实现相同的接口,该接口通常只有一个没有任何参数的执行方法,让调用者能在不和具体命令耦合的情况下使用同一请求执行不同命令。
这还有额外的好处:能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。
李星云在游戏中应用命令模式后,不再需要任何按钮子类来实现功能。
他只需要在按钮Button基类中添加一个成员变量来存储对于命令对象的引用,并在点击后执行该命令即可。
结构

实现
Java
/**
* 抽象命令
*
*/
public abstract class Command {
public Editor editor;
private String backup;
Command(Editor editor) {
this.editor = editor;
}
void backup() {
backup = editor.textField.getText();
}
public void undo() {
editor.textField.setText(backup);
}
public abstract boolean execute();
}
/**
* 拷贝命令
*
*/
public class CopyCommand extends Command {
public CopyCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
editor.clipboard = editor.textField.getSelectedText();
return false;
}
}
/**
* 粘贴命令
*
*/
public class PasteCommand extends Command {
public PasteCommand(Editor editor) {
super(editor);
}
@Override
public boolean execute() {
if (editor.clipboard == null || editor.clipboard.isEmpty()) return false;
backup();
editor.textField.insert(editor.clipboard, editor.textField.getCaretPosition());
return true;
}
}
/**
* 编辑器
*
*/
public class Editor {
public JTextArea textField;
public String clipboard;
public void init() {
JFrame frame = new JFrame("文本编辑器");
JPanel content = new JPanel();
frame.setContentPane(content);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
textField = new JTextArea();
textField.setLineWrap(true);
content.add(textField);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
JButton ctrlC = new JButton("Ctrl+C");
JButton ctrlX = new JButton("Ctrl+X");
JButton ctrlV = new JButton("Ctrl+V");
JButton ctrlZ = new JButton("Ctrl+Z");
Editor editor = this;
// 拷贝命令
ctrlC.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new CopyCommand(editor));
}
});
// 粘贴命令
ctrlV.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
executeCommand(new PasteCommand(editor));
}
});
buttons.add(ctrlC);
buttons.add(ctrlV);
content.add(buttons);
frame.setSize(450, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
/**
* 客户端
*
*/
public class Client {
public static void main(String[] args) {
Editor editor = new Editor();
editor.init();
}
}
Go
package main
import "fmt"
/**
* 命令接口
*
*/
type Command interface {
execute()
}
/**
* Button功能按钮
*
*/
type Button struct {
command Command
}
func (b *Button) press() {
b.command.execute()
}
/**
* 具体命令
*
*/
type OnCommand struct {
device Device
}
func (c *OnCommand) execute() {
c.device.on()
}
type OffCommand struct {
device Device
}
func (c *OffCommand) execute() {
c.device.off()
}
/**
* 命令接收者接口
*
*/
type Device interface {
on()
off()
}
/**
* 具体接收者:TV
*
*/
type Tv struct {
isRunning bool
}
func (t *Tv) on() {
t.isRunning = true
fmt.Println("打开电视")
}
func (t *Tv) off() {
t.isRunning = false
fmt.Println("关闭电视")
}
/**
* 客户端
*
*/
func main() {
tv := &Tv{}
onCommand := &OnCommand{
device: tv,
}
offCommand := &OffCommand{
device: tv,
}
onButton := &Button{
command: onCommand,
}
onButton.press()
offButton := &Button{
command: offCommand,
}
offButton.press()
}
适用场景
当需要通过操作来参数化对象时,可以使用命令模式。
如果想要实现撤销(Undo)操作和恢复(Redo)操作,可以使用命令模式。
当需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互时,可以使用命令模式。
优缺点
命令模式的优点。
降低系统的耦合度,符合
单一职责原则
。可以在不修改已有代码的情况下创建新的命令,符合
开闭原则
。可以实现撤销、恢复、操作历史和操作的延时等功能。
可以比较容易地设计一个命令队列或者宏命令(组合命令)。
命令模式的缺点。
- 因为针对每一个命令都需要设计一个具体的命令类,所以可能会导致某些系统有过多的具体命令类,这会让程序变得更复杂。
相关性
责任链模式
、命令模式
、中介者模式
和观察者模式
都用于处理请求发送者和接收者之间的不同连接方式。责任链模式
的管理者可使用命令模式
实现,此时可以对请求代表的同一个上下文对象执行许多不同的操作。可以同时使用
命令模式
和备忘录模式
来实现“撤销”。此时命令模式
用于对目标对象执行各种不同的操作,而备忘录模式
则用来保存一条命令执行前对象的状态。命令模式
和策略模式
看上去很像,两者都能通过某些行为来参数化对象,但它们的意图不同:可以使用命令模式
来将任何操作转换为对象,而策略模式
通常用于描述完成操作的不同方式,且能够在同一个上下文中切换操作。原型模式
可用于保存命令模式
的历史记录。可以将
访问者模式
视为命令模式
的加强版本,它可对不同类的多种对象执行操作。
感谢支持
更多内容,请移步《超级个体》。