16_命令模式 Command Pattern
1. 基本信息
中文名称: 命令模式
英文名称: Command Pattern
模式类型: 行为型设计模式
Rust 中常见实现方式: enum 命令、trait Command、Box<dyn Command>、闭包、函数指针、任务队列
命令模式的核心是:把一次操作封装成一个独立对象,使操作可以被保存、传递、排队、撤销或延迟执行。
在 Rust 中,命令模式常见有两种写法:
1. enum + match:适合命令种类固定的场景
2. trait + Box<dyn Command>:适合命令种类需要动态扩展的场景
2. 模式核心思想
命令模式的核心思想是:
把“请求执行某个操作”这件事封装起来,
让调用方不直接执行具体逻辑,而是把命令交给执行器处理。
也就是说,命令模式关注的是:
谁发起命令?
命令代表什么操作?
谁执行命令?
命令能不能保存、撤销、排队、重放?
例如:
点击按钮
执行菜单操作
提交任务
发送请求
撤销编辑
运行 CLI 子命令
这些都可以看成“命令”。
3. 这个模式解决什么问题
命令模式主要解决的是:如何把操作本身从调用方中抽离出来,使操作可以像数据一样被管理。
在实际开发中,经常会遇到:
1. 一个操作不想立即执行,而是之后执行。
2. 操作需要放入任务队列。
3. 操作需要记录日志。
4. 操作需要支持撤销和重做。
5. 调用方不想直接依赖具体执行逻辑。
6. 多个操作需要统一调度。
如果没有命令模式,调用方可能会直接调用各种业务函数:
create_file()
delete_file()
rename_file()
copy_file()
当这些操作需要排队、撤销、记录、重放时,直接函数调用就不够灵活。
命令模式的价值在于:
把操作封装成命令对象,
使操作可以被保存、传递、组合和统一执行。
4. 不使用这个模式会怎样
如果不使用命令模式,可能会出现以下问题:
1. 调用方直接依赖具体操作函数。
2. 操作逻辑和触发逻辑耦合在一起。
3. 很难实现撤销、重做、日志记录。
4. 很难把操作放入队列延迟执行。
5. 新增操作时,需要修改调用方大量代码。
6. 多种操作缺少统一表示方式。
例如 GUI 按钮直接调用具体函数:
保存按钮 → save_file()
删除按钮 → delete_file()
复制按钮 → copy_file()
短期看很简单,但如果以后要增加:
快捷键
菜单栏
撤销功能
操作记录
脚本化执行
就会发现每个入口都要重复绑定具体逻辑。
命令模式可以让这些入口都只产生“命令”,再由统一执行器处理。
5. 传统面向对象中的实现思路
在传统 OOP 中,命令模式通常包含几个角色:
1. Command:命令接口,定义 execute() 方法。
2. ConcreteCommand:具体命令,实现具体操作。
3. Receiver:真正执行业务逻辑的对象。
4. Invoker:命令调用者,负责触发命令。
5. Client:创建命令并绑定接收者。
典型结构是:
Command
└── execute()
CreateFileCommand
├── receiver: FileSystem
└── execute() { receiver.create_file() }
Invoker
└── command.execute()
这里有一个关键点:
命令对象本身不一定真正完成所有业务逻辑,
它通常只是封装“调用哪个接收者的哪个方法”。
例如:
按钮 Button 不知道怎么保存文件。
按钮只知道执行一个 Command。
真正保存文件的是 FileSystem。
6. Rust 中的实现思路
6.1 Rust 中通常怎么实现
Rust 中命令模式有两种常见实现方式。
第一种是 enum + match。
如果命令种类是固定的,Rust 更推荐用 enum 表达命令:
enum Command {
CreateFile { name: String },
DeleteFile { name: String },
RenameFile { old: String, new: String },
}
struct FileSystem;
impl FileSystem {
fn execute(&self, command: Command) {
match command {
Command::CreateFile { name } => {
println!("创建文件:{}", name);
}
Command::DeleteFile { name } => {
println!("删除文件:{}", name);
}
Command::RenameFile { old, new } => {
println!("重命名文件:{} -> {}", old, new);
}
}
}
}
fn main() {
let fs = FileSystem;
let cmd = Command::CreateFile {
name: "test.txt".to_string(),
};
fs.execute(cmd);
}
这种写法的优点是:
1. 命令类型明确。
2. match 可以穷尽检查。
3. 不需要动态分发。
4. 适合 CLI、任务类型固定的系统。
第二种是 trait + Box<dyn Command>。
如果命令类型需要动态扩展,可以定义 Command trait:
trait Command {
fn execute(&self);
}
struct CreateFileCommand {
name: String,
}
impl Command for CreateFileCommand {
fn execute(&self) {
println!("创建文件:{}", self.name);
}
}
struct DeleteFileCommand {
name: String,
}
impl Command for DeleteFileCommand {
fn execute(&self) {
println!("删除文件:{}", self.name);
}
}
struct Invoker {
commands: Vec<Box<dyn Command>>,
}
impl Invoker {
fn new() -> Self {
Self {
commands: Vec::new(),
}
}
fn add_command(&mut self, command: Box<dyn Command>) {
self.commands.push(command);
}
fn run(&self) {
for command in &self.commands {
command.execute();
}
}
}
fn main() {
let mut invoker = Invoker::new();
invoker.add_command(Box::new(CreateFileCommand {
name: "a.txt".to_string(),
}));
invoker.add_command(Box::new(DeleteFileCommand {
name: "b.txt".to_string(),
}));
invoker.run();
}
这种写法的优点是:
1. 可以把不同命令放入同一个队列。
2. 可以运行时决定执行哪些命令。
3. 可以扩展新的命令类型。
4. 适合任务队列、插件化系统、GUI 事件系统。
6.2 和传统 OOP 写法相比有什么不同
和传统面向对象语言相比,Rust 的命令模式有几个明显特点。
第一,Rust 可以直接用 enum 表达命令集合。
传统 OOP 中往往一个命令一个类;Rust 中如果命令种类有限,用 enum 会更简洁。
第二,Rust 的 match 可以进行穷尽性检查。
当新增命令类型时,编译器会提示哪些地方没有处理。
第三,Rust 也可以用 trait object 实现传统命令对象。
例如 Box<dyn Command> 可以把不同命令放到同一个 Vec 中统一执行。
第四,Rust 中命令对象要考虑所有权。 命令如果携带数据,比如文件名、参数、请求体,需要明确这些数据是移动进命令、借用,还是共享。
第五,Rust 中闭包也可以作为轻量命令。 如果命令逻辑很简单,不一定要定义结构体,可以直接保存闭包。
6.3 Rust 中是否有更自然的替代写法
在 Rust 中,命令模式可以根据场景选择不同写法。
如果命令集合固定,推荐:
enum + match
如果命令集合需要扩展,推荐:
trait Command + Box<dyn Command>
如果命令只是简单回调,推荐:
闭包
例如:
#![allow(unused)]
fn main() {
let commands: Vec<Box<dyn Fn()>> = vec![
Box::new(|| println!("执行命令 A")),
Box::new(|| println!("执行命令 B")),
];
for command in commands {
command();
}
}
如果需要撤销操作,可以让命令同时支持:
execute()
undo()
如果需要异步任务队列,可以考虑:
channel
async task
tokio::spawn
所以 Rust 中的命令模式不一定非要写成复杂的“命令类体系”,而是要根据命令是否固定、是否需要扩展、是否需要撤销来选择实现方式。
7. Rust 中涉及的语言特性
命令模式在 Rust 中主要涉及以下语言特性:
1. enum
2. match
3. trait
4. `Box<dyn Trait>`
5. Vec
6. 闭包
7. 所有权移动
8. Result
9. channel
10. async / await
这些特性分别解决的问题是:
enum:表达固定命令集合
match:根据命令类型分发执行逻辑
trait:抽象命令接口
Box<dyn Trait>:保存不同类型命令
Vec:管理命令队列或历史记录
闭包:表达轻量命令
所有权:管理命令携带的数据
Result:表达命令执行成功或失败
channel:实现跨线程命令传递
async:实现异步命令执行
其中最核心的是:
enum + match
trait Command + Box<dyn Command>
8. 性能与工程代价
命令模式的性能取决于具体实现方式。
如果使用 enum + match:
1. 没有动态分发。
2. 没有额外堆分配。
3. 编译器容易优化。
4. 适合命令类型固定的高性能场景。
如果使用 Box<dyn Command>:
1. 可以统一保存不同命令类型。
2. 有动态分发开销。
3. 通常会产生堆分配。
4. 灵活性更强,但性能略低。
如果使用闭包:
1. 简单灵活。
2. 可以捕获环境变量。
3. 使用 Box<dyn Fn()> 时同样可能有动态分发和堆分配。
工程代价方面,命令模式会引入额外结构:
1. 需要定义命令类型。
2. 需要设计执行器或调度器。
3. 如果支持 undo,需要额外保存历史。
4. 如果命令携带资源,需要处理所有权和生命周期。
一句话总结:
命令模式提升了操作管理能力,但会增加一层命令封装。
9. 典型应用场景
命令模式适合用于“操作需要被管理”的场景。
常见应用包括:
1. GUI 按钮事件
2. 菜单操作
3. 命令行子命令
4. 任务队列
5. 撤销和重做系统
6. 操作日志
7. 宏命令
8. 消息队列
9. 异步任务调度
10. 游戏输入控制
在 Rust 项目中,命令模式可能出现在:
CLI 工具
Web 请求任务处理
批量编译任务调度
文件操作工具
编辑器撤销系统
异步 worker 队列
测试任务执行器
例如批量编译 crate 时,可以把每个任务封装成命令:
CompileCommand
TestCommand
CleanCommand
UpdateDependencyCommand
然后统一放入队列执行。
10. 和相似模式的区别
命令模式容易和策略模式、责任链模式、备忘录模式、观察者模式混淆。
10.1 命令模式和策略模式
二者都可能把行为封装起来,但关注点不同。
策略模式:关注算法或行为的替换。
命令模式:关注操作的封装、传递、排队、撤销和延迟执行。
策略模式更像是:
我选择哪种算法来完成这个任务。
命令模式更像是:
我把这个任务封装成一个命令,交给别人执行。
例如:
选择 zip 或 gzip 压缩:策略模式
把“压缩这个文件”放进任务队列:命令模式
10.2 命令模式和责任链模式
二者都可能处理请求,但结构不同。
命令模式:把请求封装成对象,由执行器执行。
责任链模式:请求沿着多个处理者传递。
命令模式关注:
请求本身如何被表示和管理。
责任链模式关注:
请求应该由链上的哪个处理者处理。
例如:
保存文件命令加入操作队列:命令模式
HTTP 请求经过认证、日志、路由处理:责任链模式
10.3 命令模式和备忘录模式
二者都可以实现撤销,但方式不同。
命令模式:保存操作,通过反向操作 undo。
备忘录模式:保存状态,通过恢复快照 undo。
例如:
删除字符命令的 undo 是重新插入字符:命令模式
保存整个文档历史状态再恢复:备忘录模式
如果操作很容易反向执行,命令模式更合适。 如果操作难以反向计算,备忘录模式更合适。
10.4 命令模式和观察者模式
二者都可能和事件系统有关,但关注点不同。
命令模式:事件发生后要执行什么操作。
观察者模式:状态变化后要通知哪些对象。
例如:
按钮点击后执行 SaveCommand:命令模式
文档变化后通知多个视图刷新:观察者模式
观察者模式更强调通知关系,命令模式更强调操作封装。
11. 使用该模式的优点
命令模式的优点主要有:
1. 将操作请求和执行逻辑解耦。
2. 命令可以被保存、排队、延迟执行。
3. 可以支持撤销和重做。
4. 可以统一管理不同操作。
5. 可以记录操作日志或实现重放。
6. 在 Rust 中可以用 enum 或 trait 灵活实现。
从工程角度看,命令模式的价值在于:
把“行为”变成可以管理的数据。
12. 使用该模式的代价
命令模式的代价主要包括:
1. 简单操作使用命令模式可能过度设计。
2. 需要额外定义命令类型或命令 trait。
3. 动态命令可能带来堆分配和动态分发开销。
4. 支持 undo 时需要额外保存上下文。
5. 命令数量过多时,结构可能变复杂。
特别是在 Rust 中,如果命令携带大量数据,要注意:
1. 数据是移动进命令,还是借用?
2. 命令需要执行一次,还是可以多次执行?
3. 命令执行失败怎么处理?
4. 命令是否需要线程安全?
这些都会影响命令结构设计。
13. 什么时候不应该使用
以下情况不适合使用命令模式:
1. 操作非常简单,直接调用函数即可。
2. 不需要撤销、队列、延迟执行或日志记录。
3. 操作数量很少,也不会扩展。
4. 调用方和执行方没有解耦需求。
5. 命令对象反而让代码更难理解。
在 Rust 中可以遵循一个简单原则:
只是立即执行一次的简单函数,不需要命令模式。
需要把操作保存、传递、排队、撤销时,再考虑命令模式。
14. 一个简单例子思路
场景: 文本编辑器撤销操作
需求:
支持插入文本
支持删除文本
支持撤销上一步操作
处理流程:
1. 定义 Command trait。
2. 每个命令实现 execute()。
3. 如果支持撤销,再实现 undo()。
4. Editor 作为 Receiver,真正修改文本。
5. Invoker 保存命令历史栈。
6. 执行命令后,把命令放入 history。
7. undo 时,从 history 弹出命令并执行 undo()。
适合练习的 Rust 特性:
trait
Box<dyn Trait>
Vec 历史栈
所有权
可变借用
Result
也可以设计一个 CLI 场景:
Command::Add
Command::Remove
Command::List
Command::Run
这种情况下更适合用 enum + match。
15. 总结一句话
命令模式的本质是:
把一次操作封装成一个可保存、可传递、可排队、可撤销或可延迟执行的命令对象。
在 Rust 中,如果命令集合固定,通常优先使用 enum + match;如果命令需要动态扩展,可以使用 trait Command + Box<dyn Command>;如果只是简单回调,也可以直接使用闭包。命令模式适合任务队列、撤销重做、CLI 子命令、GUI 事件和异步任务调度等场景。