命令模式:将请求封装为对象
什么是命令模式
命令模式是一种行为设计模式,它将一个请求封装为一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
通俗地讲,命令模式把“发出请求的对象”和“如何执行请求的对象”完全解耦。你不再需要直接调用某个方法,而是创建一个代表该请求的命令对象,然后由调度者来调用。这样做的好处是:请求可以延迟执行、可以排队、可以记录日志,还很容易实现撤销和重做。
为什么需要命令模式
在日常开发中,我们经常遇到这样的场景:按钮点击、菜单选择、快捷键操作,这些行为通常直接耦合在界面组件里。这样会带来几个问题:
- 界面代码与业务逻辑紧密耦合,难以复用和测试。
- 难以实现操作的撤销、重做、宏命令等高级功能。
- 当需要扩展新的操作时,必须修改已有类,违背开闭原则。
命令模式通过引入命令对象,将操作本身参数化,完美解决了上述问题。
命令模式的核心角色
- Command(命令接口):声明执行操作的接口,通常包含
execute()方法,有的还会包含undo()方法。 - ConcreteCommand(具体命令):绑定一个接收者对象,调用接收者的相应操作,实现
execute()。 - Invoker(调用者/触发者):要求命令对象执行请求,它可以持有命令对象,并在合适的时机调用其
execute()。 - Receiver(接收者):真正执行具体业务逻辑的对象,任何类都可能充当接收者。
- Client(客户端):创建具体命令对象并设置它的接收者,同时将命令对象交给调用者。
命令模式的类图结构
+----------------+ +-------------------+
| Invoker | | <<interface>> |
|----------------| uses | Command |
| - command |<---------|-------------------|
| + setCommand() | | + execute() |
| + invoke() | | + undo() |
+----------------+ +-------------------+
^
|
+-----------+-----------+
| |
+------------------+ +------------------+
|ConcreteCommandA | |ConcreteCommandB |
|------------------| |------------------|
| - receiver | | - receiver |
| - params | | - params |
| + execute() | | + execute() |
| + undo() | | + undo() |
+--------+---------+ +--------+---------+
| |
v v
+------------------+ +------------------+
| Receiver | | Receiver |
|------------------| |------------------|
| + action() | | + action() |
+------------------+ +------------------+
一个简单易懂的代码示例
假设我们正在开发一个智能家居遥控器,遥控器的每个按钮可以执行不同设备的操作。这里使用命令模式来实现解耦。
1. 定义接收者:电灯与音响
// 接收者:电灯
class Light {
public void on() {
System.out.println("电灯已打开");
}
public void off() {
System.out.println("电灯已关闭");
}
}
// 接收者:音响
class Stereo {
public void on() {
System.out.println("音响已打开");
}
public void off() {
System.out.println("音响已关闭");
}
public void setVolume(int level) {
System.out.println("音响音量设置为:" + level);
}
}
2. 定义命令接口及具体命令
// 命令接口
interface Command {
void execute();
void undo();
}
// 开灯命令
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
// 关灯命令
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
// 音响开并设置音量命令(组合操作)
class StereoOnWithVolumeCommand implements Command {
private Stereo stereo;
public StereoOnWithVolumeCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.on();
stereo.setVolume(11);
}
@Override
public void undo() {
stereo.off();
}
}
3. 定义调用者:遥控器
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
4. 客户端使用
public class Client {
public static void main(String[] args) {
// 创建接收者
Light livingRoomLight = new Light();
Stereo stereo = new Stereo();
// 创建命令对象
Command lightOn = new LightOnCommand(livingRoomLight);
Command lightOff = new LightOffCommand(livingRoomLight);
Command stereoOn = new StereoOnWithVolumeCommand(stereo);
// 创建遥控器
RemoteControl remote = new RemoteControl();
// 绑定命令
remote.setCommand(lightOn);
remote.pressButton(); // 输出:电灯已打开
remote.pressUndo(); // 输出:电灯已关闭
remote.setCommand(stereoOn);
remote.pressButton(); // 输出:音响已打开 \n 音响音量设置为:11
}
}
通过这个例子可以看到,遥控器只需知道命令接口,完全不用关心具体是哪个设备、执行什么操作。新增设备时,我们只需新增对应的命令类,无需修改遥控器代码,符合开闭原则。同时,撤销功能也自然地集成在了命令内部。
命令模式的高级应用
宏命令(组合命令)
宏命令本身也是一个命令,但它内部可以包含一组命令。调用宏命令的 execute() 会依次执行内部所有命令。
class MacroCommand implements Command {
private Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (Command cmd : commands) {
cmd.execute();
}
}
@Override
public void undo() {
// 撤销时一般逆序执行
for (int i = commands.length - 1; i >= 0; i--) {
commands[i].undo();
}
}
}
宏命令非常适合实现“一键执行多个操作”的批量处理场景,比如智能家居的“回家模式”(开灯、开空调、播放音乐)。
命令队列与日志请求
命令对象可以被存入队列中,由工作线程逐一取出执行,从而实现异步任务处理。此外,可以将命令序列化到磁盘,形成操作日志,在系统崩溃后可以重新加载并执行日志中的命令来恢复状态。
撤销/重做(Undo/Redo)
通过维护一个命令历史栈,可以轻松实现多步撤销和重做。执行命令时将其压入历史栈;撤销时弹出并调用 undo();还可以将撤销的命令暂存到另一个重做栈中,支持 redo()。
命令模式的优缺点
优点
- 解耦请求者和执行者:调用者无需了解接收者的具体实现,只需操作命令接口。
- 易扩展:新增命令只需实现接口,对已有系统无影响,符合开闭原则。
- 支持撤销/重做:命令对象内部可以封装状态,实现回滚操作。
- 可将命令组合:支持宏命令,构建复杂操作。
- 可对命令进行排队、记录日志:命令对象可持久化,用于日志记录、事务等。
缺点
- 类数量可能膨胀:每个具体操作都需要一个命令类,系统复杂度会略微增加。
- 增加了一层间接性:对于简单调用场景,可能会显得过度设计。
适用场景
- 需要将操作参数化,例如菜单项、按钮的回调操作。
- 需要支持撤销、重做、事务回滚等需求。
- 需要记录操作日志,以便在系统崩溃后恢复。
- 需要将请求放入队列中异步执行,或者在不同时间执行请求。
- 需要将一组操作组合成宏命令执行。
小结
命令模式的核心思想是将请求封装为对象,从而获得对请求的完全控制权。它让代码结构更加清晰,解耦了调用者和实现者,是许多框架和工具的基础设计模式(如GUI事件处理、线程池任务调度、撤销功能等)。掌握命令模式,能够帮助你在合适的场景下写出更灵活、更易维护的代码。