设计模式24-命令模式
- 写在前面
- 行为变化模式
- 命令模式的动机
- 定义与结构
- 定义
- 结构
- C++ 代码推导
- 优缺点
- 应用场景
- 总结
- 补充
- 函数对象(Functors)
- 定义
- 具体例子
- 示例:使用函数对象进行自定义排序
- 代码说明
- 输出结果
- 具体应用
- 优缺点
- 应用场景
- 命令模式(Command Pattern)
- 定义
- 实现
- 优缺点
- 应用场景
- 对比
- 选择
写在前面
行为变化模式
- 在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化。行为变化模式,将组件的行为和组件本身进行解构。从而支持组建行为的变化。实现两者之间的松耦合。
- 行为变化模式通常指的是一类设计模式,它们允许对象在运行时根据状态或环境的变化动态地改变行为。这类模式通过将算法、职责或行为的变化封装起来,使得系统更具灵活性和可扩展性。
- 行为变化模式通过封装行为、状态或算法的变化,使得系统更加灵活和可扩展。这类模式在解决动态变化需求、减少代码复杂性、提高系统的可维护性等方面具有重要作用。然而,在选择具体模式时,应根据系统的实际需求和复杂度进行权衡,以避免过度设计和不必要的类增加。
典型模式
命令模式
访问器模式
命令模式的动机
- 在软件构建过程中,行为请求者与行为实现者通常呈现一种紧耦合。但在某些场合比如需要对行为进行,记录,撤销,重做等处理。这种无法抵御变化的解耦合是不合适的。
- 那么在这种情况下,如何将行为请求者与行为实现者进行解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
- 在许多应用中,程序需要向某个对象发送请求,但发送者并不知道请求的接收者是谁,也不知道请求的执行方式。为了实现请求的解耦,命令模式应运而生。命令模式的动机是将“请求”封装为对象,使得可以用不同的请求、队列或日志来参数化对象。命令模式允许请求的发送者与执行者解耦,并且提供了对请求排队、撤销/重做等功能的支持。
定义与结构
定义
命令模式(Command Pattern)是一种行为设计模式,它将一个请求封装为一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
结构
这张UML类图描述的是软件设计模式中的命令模式(Command Pattern)。命令模式是一种行为设计模式,它允许将一个请求封装为一个对象,从而使可用不同的请求、队列、日志来参数化其他对象。命令模式也支持可撤销的操作。根据这张图来详细解释命令模式的主要组成部分和它们之间的关系。
-
客户端(Client):
- 客户端是命令模式的发起者。它创建具体的命令对象,并设置命令的接收者。然后,它通过调用者(Invoker)来执行这个命令。
-
调用者(Invoker):
- 调用者对象负责执行命令。它不直接了解命令的接收者(Receiver)和命令的具体实现(即具体命令),而只是持有对命令对象的引用。调用者有一个执行命令的接口,如
Execute()
,它接受命令对象作为参数并调用命令的execute()
方法。
- 调用者对象负责执行命令。它不直接了解命令的接收者(Receiver)和命令的具体实现(即具体命令),而只是持有对命令对象的引用。调用者有一个执行命令的接口,如
-
命令(Command):
- 命令是一个接口或抽象类,它定义了执行命令的接口
execute()
。所有的具体命令类都实现这个接口,并在execute()
方法中实现具体的执行逻辑。
- 命令是一个接口或抽象类,它定义了执行命令的接口
-
接收器(Receiver):
- 接收器是命令的实际执行者。它知道如何执行与请求相关的操作。在命令模式中,接收器通常会有一些方法(如
Action()
),这些方法会在命令执行时被调用。
- 接收器是命令的实际执行者。它知道如何执行与请求相关的操作。在命令模式中,接收器通常会有一些方法(如
-
具体命令(ConcreteCommand):
- 具体命令是命令接口的实现类。它持有对接收者的引用,并在其
execute()
方法中调用接收者的方法(如Action()
)。这样,具体命令就封装了接收者和调用的具体操作。
- 具体命令是命令接口的实现类。它持有对接收者的引用,并在其
-
状态(State)(在图中为隐式,通过
receiver
与state
的连接表示):- 状态通常不是命令模式的核心部分,但在这张图中通过
receiver
与state
的连接暗示了接收器可能与状态有关。在命令模式的实际应用中,接收器可能会维护一些状态信息,这些状态信息会在执行命令时被读取或修改。
- 状态通常不是命令模式的核心部分,但在这张图中通过
-
执行(Execute)(在图中以方法形式出现):
Execute()
方法是调用者用于执行命令的方法。它接受一个命令对象作为参数,并调用该命令对象的execute()
方法。Execute()
方法和execute()
方法名称上的差异表示了它们是不同类中的方法,但它们共同构成了命令模式的核心执行逻辑。
图中的箭头和依赖关系:
Client
指向Invoker
:表示客户端创建并设置调用者。Invoker
指向Command
:表示调用者持有对命令对象的引用。Command
指向Execute()
(方法):这是命令接口中定义的方法。Execute()
(在Invoker
中)指向Receiver
(通过具体命令):表示调用者通过命令对象间接与接收器交互。Receiver
与ConcreteCommand
之间的关系是隐式的,因为具体命令持有对接收器的引用。ConcreteCommand
指向Execute()
(在ConcreteCommand
中):这是具体命令实现execute()
方法的地方。receiver->Action();
表示在命令执行时,接收器的Action()
方法被调用。
命令模式的主要优点是解耦了调用者和接收者,增加了命令的灵活性,支持可撤销操作和宏命令等高级功能。
C++ 代码推导
下面是一个简单的命令模式实现例子,用于遥控器控制灯光的开关。
#include <iostream>
#include <memory>
#include <vector>
// 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// 接收者类
class Light {
public:
void on() {
std::cout << "Light is On" << std::endl;
}
void off() {
std::cout << "Light is Off" << std::endl;
}
};
// 具体命令类:开灯命令
class LightOnCommand : public Command {
public:
LightOnCommand(Light& light) : light_(light) {}
void execute() override {
light_.on();
}
void undo() override {
light_.off();
}
private:
Light& light_;
};
// 具体命令类:关灯命令
class LightOffCommand : public Command {
public:
LightOffCommand(Light& light) : light_(light) {}
void execute() override {
light_.off();
}
void undo() override {
light_.on();
}
private:
Light& light_;
};
// 调用者类:遥控器
class RemoteControl {
public:
void setCommand(std::shared_ptr<Command> command) {
command_ = command;
}
void pressButton() {
if (command_) {
command_->execute();
history_.push_back(command_);
}
}
void pressUndo() {
if (!history_.empty()) {
history_.back()->undo();
history_.pop_back();
}
}
private:
std::shared_ptr<Command> command_;
std::vector<std::shared_ptr<Command>> history_;
};
int main() {
Light light;
std::shared_ptr<Command> lightOn = std::make_shared<LightOnCommand>(light);
std::shared_ptr<Command> lightOff = std::make_shared<LightOffCommand>(light);
RemoteControl remote;
remote.setCommand(lightOn);
remote.pressButton(); // 打开灯
remote.setCommand(lightOff);
remote.pressButton(); // 关闭灯
remote.pressUndo(); // 撤销关闭灯,重新打开灯
return 0;
}
优缺点
优点:
- 解耦发送者与接收者:命令模式将请求的发送者与实际执行者解耦,使得可以在不修改发送者代码的情况下更改或扩展接收者。
- 增加灵活性:可以容易地将新命令加入系统,支持撤销/重做、日志记录、事务等功能。
- 组合命令:可以将多个命令组合成一个复合命令,从而实现更复杂的功能。
- 支持宏命令:可以方便地实现批处理,多个命令组合成宏命令一起执行。
缺点:
- 命令类数量可能增加:对于每个不同的操作,都需要设计一个具体命令类,这可能导致类的数量增多。
- 过度设计:对于简单的操作,命令模式可能显得过于复杂。
应用场景
- 操作的可撤销性:如文本编辑器中的撤销/重做操作。
- 事务性操作:如数据库的事务处理,需要对操作进行记录,以便在出现错误时回滚。
- 远程调用:如远程控制系统,需要将操作封装为命令,通过网络发送给远程服务器执行。
- 宏命令:如在家居自动化中,一个按键可以执行一系列命令(如同时关闭所有灯光、锁门等)。
命令模式在需要灵活性、扩展性,以及解耦请求和执行者的场景中非常有用。它不仅提高了系统的可维护性,还为功能的拓展提供了良好的支持。
总结
- 命令模式的根本目的在于将行为请求者与行为实现者进行解耦。在面向对象语言中常见的实现手段是将行为抽象为对象。
- 实现命令接口的具体命令对象,有时候根据需要可能会保存一些额外的状态信息。通过使用组合模式可以将多个命令封装为一个复合命令也就是宏命令。
- 命令模式与c++中的函数对象有些类似。但两者定义行为接口的规范有所区别。命令模式以面向对象中的接口实现来定义行为接口规范。更严格,但是具有性能损失的缺点。C++函数对象以函数签名(参数+返回值)来定义行为接口规范。更灵活性能更高。
补充
C++的函数对象(Functors)和命令模式(Command Pattern)都是将操作封装为对象的技术,但它们的实现方式、用途和适用场景有所不同。下面是它们的对比、优缺点以及应用场景的详细说明。
函数对象(Functors)
定义
在C++中,函数对象是指重载了operator()
的类对象。通过重载operator()
,类的实例能够像函数一样被调用。这种机制允许将行为封装在对象中,并使其可以在需要时调用。
具体例子
C++的函数对象(Functors)通过重载operator()
来实现,使得类对象能够像函数一样被调用。这种特性在许多场景下非常有用,比如在标准库算法中传递行为、创建灵活的回调函数等。以下是一个更具体的例子,演示如何使用函数对象来创建一个自定义的排序规则。
示例:使用函数对象进行自定义排序
假设我们有一个包含学生成绩的向量,我们希望根据学生的成绩进行排序,但如果成绩相同,则按学生的名字进行字母排序。我们可以通过定义一个函数对象来实现这个自定义的排序规则。
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// 定义一个Student结构体,包含姓名和成绩
struct Student {
std::string name;
int grade;
};
// 定义一个函数对象,用于自定义排序规则
class CompareStudents {
public:
// 重载operator(),实现自定义排序规则
bool operator()(const Student& a, const Student& b) const {
if (a.grade != b.grade) {
return a.grade > b.grade; // 成绩从高到低排序
} else {
return a.name < b.name; // 如果成绩相同,按名字字母顺序排序
}
}
};
int main() {
// 创建一个学生列表
std::vector<Student> students = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 90},
{"David", 85},
{"Eve", 92}
};
// 使用std::sort和自定义的函数对象进行排序
std::sort(students.begin(), students.end(), CompareStudents());
// 输出排序后的学生列表
for (const auto& student : students) {
std::cout << student.name << ": " << student.grade << std::endl;
}
return 0;
}
代码说明
- Student 结构体:包含两个成员变量,
name
(学生姓名)和grade
(学生成绩)。 - CompareStudents 函数对象:这个类重载了
operator()
,用于定义自定义的排序规则。- 当两个学生的成绩不相同时,按成绩从高到低排序。
- 当两个学生的成绩相同时,按名字的字母顺序进行排序。
- 排序操作:
- 使用
std::sort
函数对students
向量进行排序。 std::sort
的第三个参数是排序规则,这里传递的是CompareStudents
类的一个实例。
- 使用
输出结果
程序运行后,将按自定义规则对学生列表进行排序,并输出如下结果:
Eve: 92
Alice: 90
Charlie: 90
Bob: 85
David: 85
具体应用
在实际应用中,函数对象可以用于实现任何需要灵活行为的场景。例如:
- 排序规则:如上例,函数对象可以用于自定义排序规则。
- 回调函数:函数对象可以作为回调函数传递给其他函数或类,用于事件处理、数据处理等。
- 算法参数化:在标准库算法如
std::for_each
、std::transform
等中,函数对象可以作为参数,用于定义具体的操作。
函数对象通过类的机制实现了行为的封装和状态的管理,同时保持了类似函数的调用方式,非常适合需要灵活性和状态保持的场景。
优缺点
优点:
- 简洁:函数对象通常不需要额外的基础设施(如接口、抽象类),实现较为简洁。
- 内联性:由于函数对象通常是小型类,编译器能够更容易地进行内联优化,提升性能。
- 状态保持:函数对象可以在对象内部保存状态,且可以多次使用状态。
缺点:
- 缺乏结构化:对于复杂的行为或系统,函数对象的设计可能会变得不够结构化和灵活。
- 扩展性较差:函数对象一般用于实现简单操作,难以扩展到更复杂的行为控制。
应用场景
- 标准库算法:如
std::sort
、std::for_each
等标准库算法,常常接受函数对象作为参数。 - 简单的回调:在不需要完整命令模式的地方,可以使用函数对象来代替回调函数。
命令模式(Command Pattern)
定义
命令模式是一种行为设计模式,它将请求封装为对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
实现
以下是命令模式的一个简化实现:
#include <iostream>
#include <memory>
// 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
};
// 具体命令类
class LightOnCommand : public Command {
public:
void execute() override {
std::cout << "Light is On" << std::endl;
}
};
// 调用者类
class RemoteControl {
public:
void setCommand(std::shared_ptr<Command> command) {
command_ = command;
}
void pressButton() {
if (command_) {
command_->execute();
}
}
private:
std::shared_ptr<Command> command_;
};
int main() {
RemoteControl remote;
std::shared_ptr<Command> lightOn = std::make_shared<LightOnCommand>();
remote.setCommand(lightOn);
remote.pressButton(); // 输出 "Light is On"
return 0;
}
优缺点
优点:
- 解耦性:命令模式将请求的发送者与接收者解耦,使得可以轻松地交换、增加或删除命令。
- 可扩展性:可以很容易地添加新的命令,不影响其他命令的实现。
- 支持复杂功能:如命令的排队、撤销、重做、日志记录等。
缺点:
- 复杂性:实现命令模式需要额外的命令类,这会增加系统的复杂性和代码量,尤其是在简单场景下。
- 开销较大:创建命令对象和维护这些对象的生命周期需要额外的资源开销。
应用场景
- 远程操作和请求:例如远程控制设备、网络请求处理。
- 撤销/重做操作:如文本编辑器、图像处理软件中的撤销/重做功能。
- 事务管理:如数据库的事务管理,将一系列操作封装为命令对象。
对比
- 复杂性:函数对象适用于较为简单的操作,易于实现且不需要复杂的设计模式;命令模式适用于复杂的场景,需要明确的结构和解耦要求。
- 灵活性:命令模式比函数对象更灵活,适用于需要多种操作的场景,而函数对象更适合单一功能的封装。
- 状态管理:函数对象天然支持内部状态的管理;命令模式需要在设计时明确状态的存储和操作。
选择
- 当操作简单且不需要太多结构化时,使用函数对象是更简洁的选择。
- 当操作复杂、需要解耦或者需要管理操作的生命周期(如撤销、重做)时,命令模式是更好的选择。