简介
命令模式(Command Pattern)是对命令的封装,每一个命令都是一个操作:请求方发出请求要求执行一个操作;接收方收到请求,并执行操作。命令模式解耦了请求方和接收方,请求方只需请求执行命令,不用关心命令怎样被接收、怎样被操作及是否被执行等。命令模式属于行为型设计模式。
通用模板
-
创建接收者角色:该类负责具体实施或执行一个请求。
// 接收者 public class Receiver {public void action() {System.out.println("执行具体操作");} }
-
创建抽象命令角色:定义需要执行的所有命令行为。
// 抽象命令接口 public interface ICommand {void execute(); }
-
创建具体命令角色:该类内部维护一个Receiver,在其execute()方法中调用Receiver的相关方法。
// 具体命令 public class ConcreteCommand implements ICommand {// 直接创建接收者,不暴露给客户端private Receiver receiver = new Receiver();@Overridepublic void execute() {this.receiver.action();} }
-
创建请求者角色:接收客户端的命令,并执行命令。
// 请求者 public class Invoker {private ICommand cmd;public Invoker(ICommand cmd) {this.cmd = cmd;}public void action(){this.cmd.execute();} }
模板测试
- 测试代码
public class Client {public static void main(String[] args) {ConcreteCommand cmd = new ConcreteCommand();Invoker invoker = new Invoker(cmd);invoker.action();} }
- 测试结果
执行具体操作
应用场景
在日常生活中,命令模式是很常见的。比如,经历过黑白电视机年代的小伙伴应该都有过这样的经历。那个年代在看电视的时候,想要换个频道简直不容易。我们得走到电视机前扭动换台的旋钮,一顿“咔咔咔”折腾才能完成频道的切换。如今,遥控器的发明简直就是“解放战争”,我们躺在沙发上只需要轻轻一按遥控器即可完成频道的切换。这就是命令模式,将换台请求和换台处理完全解耦了。
当系统的某项操作具备命令语义,且命令实现不稳定(变化)时,可以通过命令模式解耦请求与实现,使用抽象命令接口使请求方的代码架构稳定,封装接收方具体命令的实现细节。接收方与抽象命令接口呈现弱耦合(内部方法无须一致),具备良好的扩展性。命令模式主要适用于以下应用场景。 (1)现实语义中具备“命令”的操作(如命令菜单、Shell命令等)。
(2)请求的调用者和接收者需要解耦,使得调用者和接收者不直接交互。
(3)需要抽象出等待执行的行为,比如撤销(Undo)操作和恢复(Redo)等操作。
(4)需要支持命令宏(即命令组合操作)。
优点
(1)通过引入中间件(抽象接口),解耦了命令请求与实现。
(2)扩展性良好,可以很容易地增加新命令。
(3)支持组合命令,支持命令队列。
(4)可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
缺点
(1)具体命令类可能过多。
(2)命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量;代码抽离肯定比代码聚合更难理解。
“生搬硬套”实战
场景描述
我们可以用一个遥控器控制家用电器(如电灯)作为例子。在这个场景中,遥控器是发出命令的对象,而家用电器则是执行命令的对象。
代码开发
-
创建接收者角色(这里指的是实际执行的对象灯)
// 定义接收者类,即实际执行动作的对象——Light public class Light {public void turnOn() {System.out.println("Light is on");}public void turnOff() {System.out.println("Light is off");} }
-
创建抽象命令角色:(这里指的是抽象命令接口,定义执行和取消操作)
// 定义一个命令接口 public interface ICommand {void execute();void undo(); }
-
创建具体命令角色(这里指的是开灯和关灯的具体命令实现类)
// 具体的开灯命令类 public class LightOnCommand implements ICommand{private final Light light;public LightOnCommand(Light light) {this.light = light;}@Overridepublic void execute() {light.turnOn();}@Overridepublic void undo() {light.turnOff();} }
// 具体关灯命令类 public class LightOffCommand implements ICommand {private final Light light;public LightOffCommand(Light light) {this.light = light;}@Overridepublic void execute() {light.turnOff();}@Overridepublic void undo() {light.turnOn();} }
-
创建请求者角色(这里指的就是遥控器)
// 定义一个调用者——遥控器类,它将保存命令并执行 public class RemoteControl {private ICommand command;public void setCommand(ICommand command) {this.command = command;}public void pressButton() {command.execute();}public void undo() {command.undo();} }
至此,我们就通过“生搬硬套”命令模式的模板设计出一套遥控器开关灯的案例,接下来我们进行测试:
-
测试代码
public class Client {public static void main(String[] args) {Light light = new Light();RemoteControl remote = new RemoteControl();// 设置打开电灯的命令remote.setCommand(new LightOnCommand(light));remote.pressButton(); // 执行命令:打开电灯remote.undo(); // 撤销命令:关闭电灯// 设置关闭电灯的命令remote.setCommand(new LightOffCommand(light));remote.pressButton(); // 执行命令:关闭电灯remote.undo(); // 撤销命令:打开电灯} }
-
测试结果
Light is on Light is off Light is off Light is on
场景描述2
假如我们开发一个播放器,播放器有播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条去传达指令给播放器内核,具体传达什么指令,会被封装为一个个按钮。那么每个按钮就相当于对一条命令的封装。用控制条实现了用户发送指令与播放器内核接收指令的解耦。
代码开发2
-
创建接收者角色(这里指的是实际执行的对象播放器)
// 定义接收者类,即实际执行动作的对象——播放器 public class GPlayer {public void play() {System.out.println("正常播放");}public void speed() {System.out.println("拖动进度条");}public void stop() {System.out.println("停止播放");}public void pause() {System.out.println("暂停播放");} }
-
创建抽象命令角色:(这里指的是抽象命令接口,定义执行命令操作)
// 定义一个命令接口 public interface IAction {void execute(); }
-
创建具体命令角色(这里指的是播放、暂停、停止、加速具体命令实现类)
// 具体的播放命令 public class PlayAction implements IAction {private GPlayer player;public PlayAction(GPlayer player) {this.player = player;}@Overridepublic void execute() {player.play();} }
// 具体的暂停命令 public class PauseAction implements IAction {private GPlayer player;public PauseAction(GPlayer player) {this.player = player;}@Overridepublic void execute() {player.pause();} }
// 具体的停止命令 public class StopAction implements IAction {private GPlayer player;public StopAction(GPlayer player) {this.player = player;}@Overridepublic void execute() {player.stop();} }
// 具体的拖动进度条命令 public class SpeedAction implements IAction {private GPlayer player;public SpeedAction(GPlayer player) {this.player = player;}@Overridepublic void execute() {player.speed();} }
-
创建请求者角色(这里指的就是播放器控制面板)
import java.util.ArrayList; import java.util.List;// 定义一个调用者——控制面板类,它将保存命令并执行 public class Controller {private List<IAction> actions = new ArrayList<>();public void addAction(IAction action) {actions.add(action);}public void execute(IAction action) {action.execute();}public void executes() {for (IAction action : actions) {action.execute();}actions.clear();} }
至此,我们就通过“生搬硬套”命令模式的模板设计出一套通过播放器控制面板上的按钮来控制播放器的案例,接下来我们进行测试:
- 代码测试
public class Client {public static void main(String[] args) {GPlayer player = new GPlayer();Controller controller = new Controller();controller.execute(new PlayAction(player));controller.addAction(new PauseAction(player));controller.addAction(new PlayAction(player));controller.addAction(new StopAction(player));controller.addAction(new SpeedAction(player));controller.executes();} }
- 测试结果
正常播放 暂停播放 正常播放 停止播放 拖动进度条
总结
在软件系统中,行为请求者与行为实现者通常是一种紧耦合关系,因为这样的实现简单明了。但紧耦合关系缺乏扩展性,在某些场合中,当需要对行为进行记录、撤销或重做等处理时,只能修改源码。而命令模式通过在请求与实现间引入一个抽象命令接口,解耦了请求与实现,并且中间件是抽象的,它由不同的子类实现,因此具备扩展性。所以,命令模式的本质是解耦命令请求与处理。