1. 简介
命令模式(Command Pattern)是一种行为设计模式,它将一个请求封装为一个对象,从而让你可以使用不同的请求把客户端参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式通常用于以下几种情况:
-
解耦 调用操作的客户与执行操作的类:通过命令模式,可以使得调用操作的客户不需要知道是谁将会执行这个操作,以及如何执行。
-
需要对操作进行记录、排队或 日志记录:命令模式允许系统将请求记录到日志中,支持事务的撤销和恢复。
-
需要支持撤销和重做操作:命令模式可以很容易地实现操作的撤销和重做。
2. 结构
命令模式通常包含以下角色:
-
Command:定义命令的接口,声明执行操作的方法。
-
ConcreteCommand:Command 接口的实现对象,它对应于具体的行为和接收者的绑定。
-
Client:创建具体的命令对象,并且设置它的接收者。
-
Invoker:要求命令对象执行请求。
-
Receiver:知道如何实施与执行一个请求相关的操作。
3. 简单实现
3.1 工程结构
java
com.xiaokai/
├── command/
│ ├── LightOnCommand.java // 具体命令实现,用于打开灯光
│ └── Command.java // 命令接口
├── receiver/
│ └── Light.java // 接收者,执行实际的开关灯操作
├── RemoteControl.java // 调用者,用于触发命令
└── Client.java // 客户端,用于创建命令对象和调用者对象
3.2 实现
定义命令接口
java
/**
* Author:yang
* Date:2024-09-24 11:12
* Description:Command接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 撤销命令
*/
void undo();
}
具体命令实现
java
package com.xiaokai.command.impl;
import com.xiaokai.command.Command;
import com.xiaokai.command.receiver.Light;
/**
* Author:yang
* Date:2024-09-24 11:14
* Description:具体命令
*/
public class LightOnCommand implements Command {
private Light light;
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
public LightOnCommand(Light light) {
this.light = light;
}
}
接收者
java
package com.xiaokai.command.receiver;
/**
* Author:yang
* Date:2024-09-24 11:15
* Description:接收者
*/
public class Light {
public void on(){
System.out.println("Light is on");
}
public void off(){
System.out.println("Light is off");
}
}
调用者
java
package com.xiaokai.command;
/**
* Author:yang
* Date:2024-09-24 11:18
* Description:调用者
*/
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
/**
* 调用命令对象的方法
*/
public void buttonWasPressed() {
System.out.println("Button was pressed");
command.execute();
}
}
客户端
java
/**
* Author:yang
* Date:2024-09-24 11:20
* Description:客户端
*/
public class Client {
public static void main(String[] args) {
// 创建接收者对象
Light receiver = new Light();
// 创建命令对象
Command command = new LightOnCommand(receiver);
// 创建调用者对象
RemoteControl control = new RemoteControl();
control.setCommand(command);
// 执行命令
control.buttonWasPressed();
// 取消命令
command.undo();
}
}
测试结果
bash
Button was pressed
Light is on
Light is off
4. 源码
在package java.util.concurrent包中存在一个典型的命令模式应用。
命令接口
java
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
命令相关执行
java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
分 3 个步骤进行:
-
如果正在运行的线程少于 corePoolSize,请尝试使用给定命令作为其第一个任务启动新线程。对 addWorker 的调用以原子方式检查 runState 和 workerCount,从而通过返回 false 来防止错误警报,这些错误警报会在不应该添加线程时添加线程。
-
如果一个任务可以成功排队,那么我们仍然需要仔细检查我们是否应该添加一个线程(因为自上次检查以来,现有的线程已经死亡),或者池在进入此方法后关闭了。因此,我们重新检查 state,并在必要时回滚 enqueueing(如果已停止),或者如果没有,则启动一个新线程。
-
如果我们无法将任务排队,那么我们尝试添加新线程。如果失败,我们知道我们已经关闭或饱和,因此拒绝了任务。
5. 应用场景
-
智能家居控制系统:通过一个中央应用程序控制家里的各种设备,如灯光、温控器和安全摄像头。每个控制命令都封装为一个对象,允许系统排队、顺序执行和必要时撤销命令。这种模式使得控制逻辑与设备实现解耦,便于添加新设备或功能。
-
图形用户界面 ( GUI ):在桌面应用程序中,GUI按钮和菜单项通常会使用命令模式。命令模式允许用户界面与业务逻辑解耦,使得用户界面可以独立于业务逻辑进行开发和修改。
-
数据库事务:在数据库系统中,事务可以被视为一系列命令的集合,这些命令可以被提交(执行)或回滚(撤销)。命令模式提供了一种结构化的方式来管理这些操作,确保数据的一致性和完整性。
-
宏命令:在需要执行一系列命令作为单个操作时,可以将多个命令对象组合成一个宏命令。例如,在文本编辑器中,可以将一系列编辑操作(如复制、粘贴、格式设置)组合成一个宏,以便一次执行。
-
命令日志:在某些系统中,可能需要记录所有执行的命令以便于问题调试或恢复。命令模式允许系统将每个命令的执行记录到日志中,以便在系统崩溃后可以重新执行这些命令来恢复状态。
-
远程控制应用程序:例如,一个用于家庭自动化的远程控制应用程序,它可以接受命令来控制家中的各种设备,如灯光、风扇等。命令模式允许将每个设备的操作封装为命令对象,使得远程控制应用程序可以灵活地添加或修改控制命令。
-
文本编辑器:文本编辑器中的撤销和重做功能通常使用命令模式实现。每个编辑操作(如插入文本、删除文本)都是一个命令对象,可以被执行、撤销和重做。
-
Java
Runnable
接口 :Java中的Runnable
接口是一种特殊的命令模式,它允许将任务封装为对象,这些对象可以被提交给线程池执行。 -
Spring框架的
JdbcTemplate
:Spring框架使用命令模式来执行数据库操作。JdbcTemplate
使用命令对象来封装SQL语句和相关的参数,然后执行这些命令。