命令模式 Command
命令模式概述
- 什么是命令模式: 是一种行为型设计模式,将命令与具体实现进行解耦,将命令封装为对象数据进行传递,命令只关注与"执行",“撤销执行”,而命令具体执行的是什么,有下一层的接收者根据命令对象的不同来决定
- 命令模式的优点: 将请求与具体行为解耦,提高扩展性方便添加新的命令,可以比较容易的设置组合命令,记录命令,通过记录命令实现命令撤销
分析命令模式中的角色
-
接收者Receiver: 具体产品功能类,执行功能动作
-
命令Command: 为所有命令抽象的的父接口,提供执行命令的execute()与撤销执行命令的undo()方法
-
具体命令一 ConcreteCommand1: 空命令,根据需求,不是必须创建的,实现命令接口,重写空的执行与撤销执行方法,在初始化时,并未真实对命令进行设置,此时执行命令调用的是空命令中的空方法
-
思考问题: 命令与功能进行解耦后,命令只关注与"执行",“撤销执行”,那命令执行的是什么? 命令执行的功能功能是什么?就好像上面,实现了命令接口的空命令,命令执行,执行的是空方法,也就是什么都不执行根据产品与功能,实现命令接口创建"产品功能命令执行"子类,重写命令执行抽象方法"产品功能命令"子类中持有产品对象,命令执行方法中通过产品对象调用功能方法,由此处可以看出,当一个系统中功能命令过多时,使用命令模式,则会造成命令类过于庞大
-
具体命令二 Concrete1Command1: 可能有多个,根据产品与功能两个维度创建的,我理解为命令与指定功能动作的绑定,该角色持有接收者,实现命令接口,重写执行命令与撤销命令方法,当执行命令是通过持有的接受者对象调用指定功能方法
-
请求者Invoker: 我理解为用来存放不同命令的容器,同时也是客户端触发命令执行,客户端与命令的解耦层,定义了用来存放不同类型命令的容器,定义了初始化命令的方法,设置命令的方法,与执行命令的方法
命令模式流程图(不是UML)
命令模式代码示例
案例与分析
- 案例: 通过命令模式设计遥控器开灯关灯
- 产品功能: “开灯”,“关灯”
- 命令: “执行”,“撤销执行”
- 创建产品功能类,对应角色Receiver
//创建"灯",灯可以实现"开灯","关灯"两个功能
class LightReceiver{
public void on() {
System.out.println("打开电灯");
}
public void off() {
System.out.println("关闭电灯");
}
}
- 创建命令父接口,对应角色Command
//创建执行命令接口:抽象出接口的原因: 为了以后更好的扩展,
//现在的示例是只针对"灯"发送命令执行,假设后续需求增加
//要对电视,洗衣机...也发送命令怎么办?
//命令与功能进行解耦后,命令只关注执行与停止撤销执行,所以定
//义两个抽象方法,根据需求来的,例如在某些场景下,解耦后只有
//"执行"一种命令,"执行打开","执行关闭","执行xxx"都是执行
abstract class Command{
//执行动作
abstract void execute();
//撤销动作
abstract void undo();
}
- 创建具体命令实现子类一,空命令,对应角色ConcreteCommand1
//实现命令接口创建执行命令的实现子类一"空命令"类
//创建空命令的原因是在初始存放执行命令的容器
//时,并没有实际存放命令,专门用初始化命令的
//假设需求中在初始化命令容器时可以直接指定存放的
//命令类型,则可以不使用该类
//执行时命令对象调用执行方法,则执行此处的空方法
class NoCommand extends Command{
@Override
public void execute() {
}
@Override
public void undo() {
}
}
- 根据产品功能创建具体命令实现子类,命令与执行的动作进行绑定,对应角色Concrete1Command1:“灯"有两个功能"开灯”,“关灯”,执行灯的命令就是"执行开灯",“执行关灯”
开灯命令类
//根据产品与功能创建执行命令实现子类二: "开灯命令类",该类中重点关注"开灯"功能
class LightOnCommand extends Command{
//持有灯对象
public LightReceiver light;
//通过构造器赋值
public LightOnCommand(LightReceiver light) {
this.light = light;
}
//重写执行命令接口: 该命令为"开灯"命令
//当执行该命令时,通过持有的产品对象"灯"light
//调用开灯方法 on()
@Override
public void execute() {
light.on();
}
//重写撤销执行方法,通过light调用关灯方法
@Override
public void undo() {
light.off();
}
}
关灯命令类
//根据产品与功能创建"执行命令"实现子类三: "关灯命令"类,该类中重点关注"关灯"功能
class LightOffCommand extends Command{
//持有产品对象
public LightReceiver light;
//通过构造器赋值
public LightOffCommand(LightReceiver light) {
this.light = light;
}
//重写命令执行方法,方法中通过产品调用功能方法
@Override
public void execute() {
light.off();
}
//重写撤销命令方法,方法中通过产品调用执行前的方法
@Override
public void undo() {
light.on();
}
}
- 创建持有命令,接收客户端请求,通过持有命令,执行功能的遥控器 对应角色Invoker
//遥控器,我理解为用来存放不同命令的容器,
//同时也是客户端触发命令执行,客户端与命令的解耦层
//定义了用来存放不同类型命令的容器,定义了初始化命令的方法
//设置命令的方法,与执行命令的方法
class RemoteController{
//存放命令的容器(此处使用数组,分别定义3个,用来存放不同类型的命令)
//存放"开"命令容器
public Command[] onCommands;
//存放"关"命令容器
public Command[] offCommands;
//记录上次执行的命令
public Command undoCommand;
//构造器完成命令初始化,注意由于在最初初始化时,并不知道
//该"开"或"关"的命令是针对哪个设备的,所以使用NoCommand
//假设遥控器有5个按钮,所以初始化了5个命令
public RemoteController() {
onCommands = new Command[5];
offCommands = new Command[5];
for(int i = 0; i<5; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
}
//给指定按钮设置需要的命令(设置命令)
public void setCommand(int num, Command onCommand, Command offCommand) {
onCommands[num] = onCommand;
offCommands[num] = offCommand;
}
//按下开的按钮(执行命令)
public void buttonOnCommand(int num) {
onCommands[num].execute();
//记录此次的操作,供撤销使用
undoCommand = onCommands[num];
}
//按下关的按钮(执行命令)
public void buttonOffCommand(int num) {
offCommands[num].execute();
//记录此次的操作,供撤销使用
undoCommand = offCommands[num];
}
//按下撤销(执行命令)
public void buttonUndo() {
undoCommand.undo();
}
}
- 客户端调用
public static void main(String[]args) {
//创建点灯对象
LightReceiver lightReceiver = new LightReceiver();
//创建电灯开关命令
LightOnCommand lightOn = new LightOnCommand(lightReceiver);
LightOffCommand lightOff = new LightOffCommand(lightReceiver);
//创建遥控器
RemoteController remote = new RemoteController();
//给遥控器设置命令
remote.setCommand(0, lightOn, lightOff);
//按下灯的开按钮
remote.buttonOnCommand(0);
//按下灯的撤销按钮
remote.buttonUndo();
//按下灯的关按钮
remote.buttonOffCommand(0);
//按下灯的撤销按钮
remote.buttonUndo();
}
根据代码分析
我理解的命令模式是: 将命令与实际功能进行剥离,创建命令类与功能类,命令类中提供命令执行的方法,然后新增命令与功能进行绑定的类,例如开灯命令,关灯命令,命令功能绑定类继承命令类,并持有功能类对象,重写执行命令的方法,在方法中通过持有的功能类对象调用功能方法,进而实现命令执行功能执行
业务与设计模式落地案例
- 命令模式和状态模式有一些相似之处,两者都能够将行为抽象出来并通过聚合来进行切换,区别在于目的和解决的问题
- 命令模式的目的是将请求发送者和接收者解耦,使得请求可以灵活地发出和处理,从而提高系统的灵活性和可扩展性
- 状态模式的目的是将状态的变化和状态相关的行为封装起来,使得状态的变化对外部不可见,从而避免出现复杂的分支语句,并且将状态的变化和状态相关的行为集中在一个类中,更易于理解和维护
- 通过命令设计模式将记录日志的行为与业务代码进行解耦,从而提高代码可维护性和可扩展性
- 定义一个记录日志的接口 LogCommand
public interface LogCommand {
//用于执行记录日志的操作
void execute(String message);
}
- 定义两个具体的记录日志的类,分别用于将日志输出到控制台和文件中
@Slf4j
@Component
public class ConsoleLogCommand implements LogCommand {
@Override
public void execute(String message) {
log.info("记录到控制台:{}", message);
}
}
@Slf4j
@Component
public class FileLogCommand implements LogCommand {
@Override
public void execute(String message) {
try (FileWriter writer = new FileWriter("log.txt", true)) {
writer.write(message + "\n");
log.info("记录到文件:{}", message);
} catch (IOException e) {
log.error("记录到文件失败:{}", e.getMessage());
}
}
}
- 定义一个记录日志的 Invoker 类,用于接收命令并执行
@Component
public class LogInvoker {
private final LogCommand consoleLogCommand;
private final LogCommand fileLogCommand;
public LogInvoker(ConsoleLogCommand consoleLogCommand,
FileLogCommand fileLogCommand) {
this.consoleLogCommand = consoleLogCommand;
this.fileLogCommand = fileLogCommand;
}
public void execute(LogCommand command, String message) {
command.execute(message);
}
}
- 接收请求接口
@RestController
@RequestMapping("/log")
public class LogController {
private final LogInvoker logInvoker;
public LogController(LogInvoker logInvoker) {
this.logInvoker = logInvoker;
}
@GetMapping("/console/{message}")
public String logToConsole(@PathVariable String message) {
logInvoker.execute(consoleLogCommand, message);
return "记录到控制台:" + message;
}
@GetMapping("/file/{message}")
public String logToFile(@PathVariable String message) {
logInvoker.execute(fileLogCommand, message);
return "记录到文件:" + message;
}
}