命令模式

定义

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请 求排队或者记录请求日志,可以提供命令的撤销和恢复功能

粗略理解

​ 奇怪,我对于这个命令模式就是有点疑惑,我就是不知道它到底是一个什么情况。让我简单先敲一遍,兴许就能理解问题了。

​ 先举一种简单的现实生活中的命令模式来进行理解。就是餐馆的一个点餐系统。在现实生活中,你需要去思考你到底要选择那一道菜,然后你需要去考虑把你选择的这些个菜单送给服务员,然后服务员会将这些菜送给后厨,然后用户就只需要等待。

​ 但是我们有没有考虑过没有服务员这种情况呢,如果没有服务员,我们就需要去直接与后厨进行对接。这想想都不现实对吧。在程序设计中,这其实也是一种哲学。当我们程序设计得在用户实现一个功能直接调用底层的api时,这种设计是极其糟糕的。当然我这句话的描述有点问题,主要就是这里的用户是指使用设计好的程序的用户,而不是程序设计阶段的。

​ 这种没有服务员的情况其实就是一种用户请求与具体实现之间的强耦合。再举一个甲乙方的例子来看。当甲方下发一个需求的时候,具体的乙方一般是不会看到这个甲方来直接去找到对应的员工来实现对应的功能的。相反,我们通常是把这个命令发送给乙方的对接部门,然后通过一些处理,这些命令会被分包并且下发给对应的实现部门。

​ 我好像有点理解了。所谓的命令模式,要实现的就是一个命令发出者与命令实现之间的一个解耦。这时就需要我们去进行在中间的一层代理,在点餐系统中这是服务员。在项目对接中这个可能是项目经理。但是不变的是,在这种对接中,我们解开了命令发出者和命令执行者之间的强耦合。

​ 通过这种解耦,客户将不用去后厨直接找到对应的厨师进行下单。只需要更具餐馆的规范去进行我们需求的发出,服务员负责添加这些需求到对应的列表中,而服务员接下来还负责将这些个需求按一定顺序发送给对应的后厨部门进行完成,这种分层式的设计使得用户不再需要去了解底层复杂的东西,只需要去了解餐馆提供的简单的菜单即可,其他的逻辑由下面的命令转发者和命令执行者进行操作。

​ 到这里我其实明白了之前的误解。在理解命令模式中,不要在一开始就想得太多。类似与服务员这种消息转发者能够处理一些非常简单的消息转发,就比如用户想要那道菜,服务员就通知后厨做那道菜。除此之外,用户可能还会点套餐,这是一套复杂的命令,可能需要多个处理动作配合使用,这个也是服务员这个中间层需要做的事,但是一开始我们并不需要去拘泥在这里,可以从简单的地方出发。

​ 还有,当我们需要添加菜单时,我们至少也需要去为对应的后厨添加对应的处理动作,这也是我之前存在疑惑的地方,不过现在也解开了一点了。

总的来说,我现在感觉命令模式的核心在于:命令的处理与转发,通过引入一个具有转发行为的类来进行解耦

在最简单的命令模式中,这个转发可以是一个不做任何处理的转发,其他复杂情况再说,现在先从简单的入手。

代码实例

​ 在我看来,理解命令模式中,去看代码甚至比看UML类图更加重要。至少我第一次看的视乎类图根本看不懂它在说什么。

命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Receiver: 后厨
class Kitchen {
public:
void prepareDish(const std::string& dish) {
std::cout << "Kitchen is preparing: " << dish << std::endl;
}
};

// Command: 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
};

// ConcreteCommand: 具体命令类
class PrepareDishCommand : public Command {
private:
Kitchen* kitchen;
std::string dish;
public:
PrepareDishCommand(Kitchen* k, const std::string& d) : kitchen(k), dish(d) {}
void execute() override {
kitchen->prepareDish(dish);
}
};

​ 这里的几个类就是命令模式中的基本单位:命令的基本构成。在一个具体的命令中,需要包含这个命令具体的执行部门,所以这里保留了一个后厨的一个指针,用于进行执行部门的指定。我猜想,如果在后期需要对这个后厨部门进行划分,会将这个执行部门进行派生,至于继承还是聚合组合,那不是我们现在考虑的问题。

转发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Invoker: 服务员
class Waiter {
private:
std::vector<std::shared_ptr<Command>> orders;
public:
void takeOrder(std::shared_ptr<Command> command) {
orders.push_back(command);
}
void sendOrders() {
for (const auto& order : orders) {
order->execute();
}
orders.clear();
}
};

​ 可以看到,这里的转发者维护了一个命令队列,所有的客户给出的命令将会储存在这个队列中。我们还可以观察到这里的命令实际执行情况,其实就是一次程序控制流的转移,将当前的执行权限交给命令类进行自我的执行来实现对应的功能。

​ 这里当然还可以进行一系列的扩展,就比如进行消息的撤销等操作,可以自行添加。

​ 总的来说,这里的转发类重要的是这里的命令处理方法。通过储存客户端发出的所有命令,在适当的时机通过自身设定的命令执行顺序来进行对应的执行。而这里的执行,就跟很多设计模式一样,实际上就是进行一次程序控制流的转移,将程序控制流从当前的中转类转移到对应的命令处理程序中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Client: 客户端
int main() {
// 创建接收者
Kitchen kitchen;

// 创建具体命令
auto steakCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Steak");
auto pastaCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Pasta");

// 创建调用者
Waiter waiter;
waiter.takeOrder(steakCommand);
waiter.takeOrder(pastaCommand);

// 服务员发送订单
waiter.sendOrders();

return 0;
}

​ 这里其实就没有什么好说的,需要注意的是,在命令模式中,我们需要在程序一开始进行所有的命令的初始化,之后我们所有发出的命令本质上其实就是保留了一份对应的指针。当然,你可以在每次调用时都进行副本的一次创建,不给你也应该知道这种模式的坏处。

下面给出一个不是很美的UML类图,我自己都没去看()

UML类图

img

​ 在接下来的QT中,我将会是一个相对无厘头的形态,属于是看到哪学到哪,并且大部分时间不会搓代码,只在之后使用一俩个相对不错的项目来进行熟悉。

Read more »

​ 在这个栏目中,我们将不会对于一个知识点进行系统的学习,将只会对于各个我在过程中遇到的一些问题进行一些分析。

零散知识点

const属性

​ 首先来看到一句话

当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。

​ 这句话其实涉及到了C++中对于const属性的一些分析,我们来看一下。

​ 在C++中,const声明的值是一种常量,我们通常会使用这种来做为程序中不变的量,但是我们是否考虑过这种常量是怎么来的呢。其实在CSAPP中,我们学到了一点,就是基本来说这些个常量,或者说局部静态变量在编译链接后生成的二进制文件中都存在着一块各自的内存区域。

​ 回顾一下吧,GPT真好用,我都省的去敲了。

Read more »

责任链模式

定义

​ 为请求创建一个接收者对象的链。该模式使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连接成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。

举例

​ 责任链模式其实在现实生活中非常的常见,最简单的一种架构就是一个公司中对于一个员工提出的需求的处理。就比如,这个员工可能会想要加薪,可能要请假,可能想辞职。总总这些,都是员工的一些可能的行为,在提出这些需求后,员工会将这些需求提交给他的上级。就比如他的部门的老大。

​ 他部门的老大会看当前他是否存在权利能够处理当前提出的需求,就比如如果一个员工想要请个小假,这种权利一般来说一个部门老大还是有的,相反,如果员工想要的是进行升职加薪这种,那对应的部门老大这个层级是没有权利去审批这事的。那么,我们先不去考虑哪种请求会被吞的情况。

​ 一般情况下,如果一个层级的人物没有权利去处理下一级传递上来的请求,那么这个请求将会被当前任务再次提交给上级,在这种情况下,这个相对的上级还会去考虑当前自己是否有权利去处理这件事,然后做出抉择,我是需要去进行当前时间的审批处理还是我仍然没有权利去处理这件事而导致再次的往上去传递信息。这种不对的递归往上传递信息,直到信息被处理后再进行返回的结构,就是责任链模式的一种实例。

注意

​ 可以想象到,在责任链模式中,我们不应该存在一个信息没有被处理的情况,在现实中也是如此,在生活中,一个公司的最上层应该拥有着对于下面的所有请求的处理权限,在这里也是如此。当然,在责任链模式中不同的是,现实中你可能会看到上层去处理一些下层处理的是,但是在责任链模式中,这种行为是不被允许的。

​ 你可以把责任链模式看做一层层的筛网,每层筛网的大小不同,会筛去符合当前权限的事件,如果当前筛网上还存在着时间没有处理,那么这些时间就会被转移到下一层筛网进行处理,直到所有的事件都被处理。这种对于事件的处理机制,才是责任链模式的核心。

UML类图

image-20241228094455900

​ 这里使用了我看的课的一个UML类图,这个类图中的职责其实已经写的很清楚了,都看到行为性模式了你自己也应该看的懂这个类图了,这里就简单过一遍就行。

AbstractManager类

​ 这个类是我们全部责任链上的类的基类,可以看到,这个类中定义了一系列的方法来进行我们责任对象的行为规定。其中的函数我们不需要去进行详细了解,我们来抽象俩个在这个抽象类中必不可少的几个属性。

抽象成员指针

​ 在责任链模式中,对于链上的每一个元素,我们至少要提供一种能够往上去传递信息的方法,在这里我们就是通过一个成员指针来实现它的链式结构来实现的,通过这个成员指针,我们将能通过这个成员指针去递归的进行信息的处理,还是那个问题,有些操作是无法再UML类图上展示出来的,还是得等到之后的代码去进行分析。

信息处理api

​ 在每个管理层中,都应该存在能够处理下层传递的api接口,在这里面你其实也很清楚它到底是谁,这里就不再赘述。这里使用了枚举变量来进行我们处理信息的一次分类,这个无所谓,你但可以使用自己的一套数据来进行识别。

实例子类

​ 这里面的系列抽象子类其实没有什么分析的价值,在这里面,对于具体子类,其实就是继承了父类的方法并且进行了一些对于自己信息处理的特化。唯一优点讨论价值的就是它的责任链的构建,每个子类都只关注与它联系的上一层的子类的指针,对于其他的层次,子类并不关注。

对于请求发出类,这里就不再进行分析了,没意思。

代码实例

基本属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 请求类型
enum RequestType {
Leave, // 请假
Raise, // 加薪
Resignation // 辞职
};

// 请求对象
struct Request {
RequestType type;
std::string description;

Request(RequestType t, const std::string& desc)
: type(t), description(desc) {}
};

​ 这里就是一些属性的抽离定义了,没什么好说的。

抽象处理者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 抽象处理者
class Handler {
protected:
std::shared_ptr<Handler> next; // 下一个处理者

public:
virtual ~Handler() = default;

void setNext(std::shared_ptr<Handler> nextHandler) {
next = nextHandler;
}

void handleRequest(const Request& request) {
if (canHandle(request)) {
processRequest(request);
} else if (next) {
next->handleRequest(request);
} else {
std::cout << "请求未被处理:" << request.description << std::endl;
}
}

virtual bool canHandle(const Request& request) = 0;
virtual void processRequest(const Request& request) = 0;
};

​ 可以看到,在这个处理者中,我们定义了一系列的抽象方法提供使用,其他的没什么意思,主要是这个处理请求的函数。

​ 在这个处理函数中,我们会先去考虑当前是否存在权限去进行处理,如果有就直接处理并且返回。如果没有权限,那么就去递归调用责任链条中的下一层角色,直到被返回为止。

​ 理论上,一定是存在角色能够处理我们这个事件的。当然,如果你输入的是一个额外的请求,不符合我们规定的请求,那抱歉,我们将无法处理,并且将会一直返回到底。

具体处理者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 具体处理者:部门主管
class Supervisor : public Handler {
public:
bool canHandle(const Request& request) override {
return request.type == Leave;
}

void processRequest(const Request& request) override {
std::cout << "主管批准请求:" << request.description << std::endl;
}
};

// 具体处理者:部门经理
class Manager : public Handler {
public:
bool canHandle(const Request& request) override {
return request.type == Raise;
}

void processRequest(const Request& request) override {
std::cout << "经理批准请求:" << request.description << std::endl;
}
};

// 具体处理者:CEO
class CEO : public Handler {
public:
bool canHandle(const Request& request) override {
return true; // CEO可以处理所有请求
}

void processRequest(const Request& request) override {
std::cout << "CEO批准请求:" << request.description << std::endl;
}
};

​ 这一个就更没有什么好说的了,就是一些具体的处理逻辑而已。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 主函数
int main() {
// 创建责任链
auto supervisor = std::make_shared<Supervisor>();
auto manager = std::make_shared<Manager>();
auto ceo = std::make_shared<CEO>();

supervisor->setNext(manager);
manager->setNext(ceo);

// 创建请求
Request leaveRequest(Leave, "请假一天");
Request raiseRequest(Raise, "加薪申请");
Request resignationRequest(Resignation, "辞职申请");

// 测试责任链
supervisor->handleRequest(leaveRequest);
supervisor->handleRequest(raiseRequest);
supervisor->handleRequest(resignationRequest);

return 0;
}

​ 这个也没有什么好说的,主要来看这里的日志输出

1
2
3
4
PS C:\Users\ASUS\Desktop\CS\CS_STUDY\designPattern\19-责任链模式> .\test.exe
主管批准请求:请假一天
经理批准请求:加薪申请
CEO批准请求:辞职申请

​ 可以看到,这里是正确的处理了我们提出的请求的,所以,可以看到,这种模式的作用也就在这。通过对于上层权利的不断下发,当然,上层还是保留着对应的权利的,可以实现在多个环节对于信息的处理,我们可以在各个环节中去细化信息处理的细节而不去影响其他环节上的代码。这种分包式的设计,使得各个环节上的功能更加容易扩展,同时也遵守了开放封闭原则。

归纳

责任链模式(Chain of Responsibility)是一种行为设计模式,其核心意义在于通过链式传递的方式解耦请求的发送者和处理者,从而实现系统的灵活性和可扩展性。

以下是责任链模式的具体意义:


1. 降低耦合性

  • 责任链模式通过将请求的发送者和具体处理者分离,使二者之间不再直接关联:
    • 发送者只需要将请求传递给链的起始节点,无需关心请求最终由谁处理。
    • 每个处理者只需关注自己的能力范围,并决定是否处理或将请求传递给下一个处理者。
  • 这种分离提高了系统的模块化,使得代码更易于维护。

2. 增强灵活性

  • 可以动态地调整链中的处理者以及处理顺序,而不会影响其他部分:
    • 通过新增处理者类实现新功能。
    • 通过重组链条调整优先级或职责分配。
  • 在运行时可以改变链的结构,从而适应不同的需求场景。

3. 实现职责分离

  • 每个处理者只关注自身的业务逻辑:
    • 将单一职责原则(SRP)落实到代码中,避免将多个职责耦合到一个类中。
    • 每个类的职责单一,易于测试和复用。

4. 支持请求的动态处理

  • 请求可以在责任链中

    动态传递

    ,直到某个处理者能够处理它:

    • 无需显式指定处理者,由链上的逻辑决定。
    • 这种动态决策使得请求的处理更加灵活。

5. 容错与扩展能力

  • 如果请求无法被任何处理者处理,可以通过链末端的逻辑提供默认处理方案或报错机制:
    • 避免请求被丢弃或未响应的情况。
  • 责任链模式支持扩展,例如添加一个新的处理者以处理特殊的请求,而不影响现有链条。

6. 现实中的类比

  • 责任链模式的意义也体现在现实场景中

    ,例如:

    • 公司审批流程:如请假申请,会从部门主管到经理再到CEO逐级审批。
    • 技术支持系统:用户请求会先经过一级客服,若无法处理则转给二级客服,直至请求被解决。

这种模式将复杂的职责分配抽象为“链式传递”,使流程自然且易于理解。


使用责任链模式的场景

适用场景

  1. 多个处理者可以处理同一类请求,但实际处理者不确定:
    • 如权限校验、事件分发等。
  2. 请求的处理逻辑需要解耦,避免硬编码依赖:
    • 如日志处理、UI事件处理等。
  3. 需要灵活的责任分配和动态调整:
    • 如任务分发系统、策略模式的动态实现。

不适用场景

  1. 处理链过长且复杂:
    • 过多的处理者可能导致性能下降。
  2. 请求需要明确的唯一处理者:
    • 如果明确知道请求由哪个处理者完成,则责任链模式可能不适合。

总结

责任链模式的意义在于通过链式传递,将职责分离、降低耦合,同时提高灵活性和可扩展性。它在解决复杂的职责分配问题时表现出色,尤其是在处理多层审批、事件分发等场景中,使得系统结构更清晰、更易于维护。

QT布局

​ QT中的大部分都是都是api,基本只要只要这东西的基本功能,再去用个几次,就能够比较熟练的运用了。至于布局,QT中的布局中存在三种方式,水平布局,垂直布局,栅格布局

Read more »

策略模式

意图

​ 定义一系列的算法,将它们一个个封装起来,并且使得它们可以相互替换。策略模式使得算法可以独立使用与它的用户而变化。

举例

​ 在我们使用的许多编辑器乃至编译器中,我们通常可以看到一些不同的换行符算法,有windows的\r\n,linux的\n,Mac用的\r等。将这些个换行符算法硬编码进我们使用的编辑器或者编译器软件中是不可取的。实际你也可以看到,基本所有的编辑器和编译器都有自己的切换换行符的功能。那么,原因是什么呢?

Read more »

观察者模式

由于一些原因,我需要先学观察者模式,其实就是我需要这个模式去进行课设()


感悟

​ 其实所有的设计模式如果只是在看的阶段去过一遍你的感触其实不会很深。相反,如果你在设计中因为一些设计导致出现了一些问题,需要一些设计模式来进行弥补的话,这种学习将会是很深刻的。当然啦,你如果一点设计模式都没有学过,你其实都不会意识到你需要这些设计模式。

​ 所以,在我的设想中,一个设计模式好的学习流程应该是先去系统的学一学一些设计模式,就比如创建型设计模式中的几种,然后去进行使用,在使用的阶段你其实不时就会发现你的创建型设计模式和结构型设计模式通常会出现一些问题。或者吧,也可以先依照设计模式的三原则去进行设计架构,然后你一定会发现使用三原则的话会导致的一些疑惑的。

​ 就比如我的课设,我在进行重构,想要重新规范下三原则中的单一职责原则时,我想要进行数据模块和渲染模块间的解耦,然后发现这种解耦会导致我的渲染操作不知道在哪里调用,这时我就回来看设计模式,发现这种问题可以通过行为模式中的观察者模式来解决,然后我就自我驱动的去学了,这才是一个学习的正反馈过程啊。


Read more »

代理模式

简介

最后一种结构型模式了,会赢的

​ 通过代理模式的名字我们也可以了解到这个模式的重要之处就在与这个”代理”二字。我们举几个生活中的例子吧。就比如之前闹的沸沸扬扬的俄乌冲突,其实就是一场代理人战争,乌克兰实际上就是美国的代理人,为什么需要这个代理人呢,因为本人不好下场。换句话说,使用代理人的一般场景就是自己动手的代价是无法接受的,需要通过一种手段来减少损失,这就是代理的关键之处。

Read more »

享元模式

定义

运用共享技术有效地支持大量细粒度的对象。其目的是通过共享对象来减少内存的使用,尤其是当系统中有大量相似对象时,享元模式可以帮助减少对象的创建,从而降低系统的内存消耗。

​ 具体来说,享元模式将对象的共享部分不共享部分分开,尽可能地将重复的对象共享,而将每个对象的独立部分(外部部分)保留在每个实例中。

Read more »