简介
行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。
行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象 之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。
其包含以下几个模式:
- 责任链模式:允许你讲请求沿着处理者链进行发送。收到请求后,每个处理者可对请求进行处理,或将其传递给链上的下个处理者。
- 命令模式:它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
- 迭代器模式:让你能在不暴露集合底层表现形式(列表、栈和树等)的情况下遍历集合中所有的元素。
- 中介者模式:能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。
- 备忘录模式:允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
- 观察者模式:允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。
- 状态模式:让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
- 策略模式:能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。
- 模板方法:在超类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。
- 访问者模式:将算法与其所作用的对象隔离开来。
责任链模式(Chain of Responsibility)
前言
假如你正在开发一个在线订购系统。你希望对系统访问进行限制,只允许认证用户创建订单。此外,拥有管理权限的用户也拥有所有订单的完全访问权限。
简单规划后,你会意识到这些检查必须依次进行。只要接收到包含用户凭据的请求,应用程序就可尝试对进入系统的用户进行认证。但如果由于用户凭据不正确而导致认证失败,那就没有必要进行后续检查了。
随着功能不断迭代,检查代码逐渐变得越来越混乱,修改某个检查步骤有时会影响其他的检查步骤。最糟糕的是,当你希望复用这些检查步骤来保护其他系统组件时,你只能复制部分代码,因为这些组件只需要部分而非全部的检查步骤。
系统会变得让人非常费解,而且其维护成本也会激增。你在艰难地和这些代码共处一段时间后,有一天终于决定对整个系统进行重构。
责任链模式会将特定行为转换为被称作处理者的独立对象。在上述示例中,每个检查步骤都可被抽取为仅有单个方法的类,并执行检查操作。请求及其数据则会被作为参数传递给该方法。
链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。除了处理请求外,处理者还负责沿着链传递请求。请求会在链上移动,直至所有处理者都有机会对其进行处理。
最重要的是:处理者可以决定不再沿着链传递请求,这可高 效地取消所有后续处理步骤。
在我们的订购系统示例中,处理者会在进行请求处理工作后决定是否继续沿着链传递请求。如果请求中包含正确的数据,所有处理者都将执行自己的主要行为,无论该行为是身份验证还是数据缓存。
实现结构
- 处理者(Handler)声明了所有具体处理者的通用接口。该接口通常仅包含单个方法用于请求处理,但有时其还会包含一个设置链上下个处理者的方法。
- 基础处理者(Base Handler)是一个可选的类, 你可以将所有处理者共用的样本代码放置在其中。通常情况下,该类中定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为:确定下个处理者存在后再将请求传递给它。
- 具体处理者(Concrete Handlers)包含处理请求的实际代码。每个处理者接收到请求后,都必须决定是否进行处理,以及是否沿着链传递请求。处理者通常是独立且不可变的,需要通过构造函数一次性地获得所有必要地数据。
- 客户端(Client)可根据程序逻辑一次性或者动态地生成链。值得注意的是,请求可发送给链上的任意一个处理者,而非必须是第一个处理者。
应用场景及优缺点
- 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
该模式能将多个处理者连接成一条链。接收到请求后,它会“询问”每个处理者是否能够对其进行处理。这样所有处理者都有机会来处理请求。
- 当必须按顺序执行多个处理者时,可以使用该模式。
无论你以何种顺序将处理者连接成一条链,所有请求都会严格按照顺序通过链上的处理者。
优点:
- 你可以控制请求处理的顺序。
- 单一职责原则。你可对发起操作和执行操作的类进行解耦。
- 开闭原则。你可以在不更改现有代码的情况下在程序中新增 处理者。
缺点:部分请求可能未被处理。
实例演示
在本例中,员工申请处理票据需要上报给上级,如果上级无权处理就上报给更高的上级。
Handler.h:
#ifndef HANDLER_H_
#define HANDLER_H_// 抽象处理者, 在C++中是抽象基类
class ApproverInterface {public:// 添加上级virtual void setSuperior(ApproverInterface* superior) = 0;// 处理票据申请, 参数是票据面额virtual void handleRequest(double amount) = 0;
};#endif // HANDLER_H_
BaseHandler.h:
#ifndef BASE_HANDLER_H_
#define BASE_HANDLER_H_#include <string>
#include "Handler.h"class BaseApprover : public ApproverInterface {public:BaseApprover(double mpa, std::string n) : max_processible_amount_(mpa), name_(n), superior_(nullptr) {}// 设置上级void setSuperior(ApproverInterface* superior) {superior_ = superior;}// 处理票据void handleRequest(double amount) {// 可处理时直接处理即可if (amount <= max_processible_amount_) {printf("%s处理了该票据, 票据面额:%f\n", name_.c_str(), amount);return;}// 无法处理时移交给上级if (superior_ != nullptr) {printf("%s无权处理, 转交上级...\n", name_.c_str());superior_->handleRequest(amount);return;}// 最上级依然无法处理时报错printf("无人有权限处理该票据, 票据金额:%f\n", amount);}private:double max_processible_amount_; // 可处理的最大面额std::string name_;ApproverInterface* superior_;
};#endif // BASE_HANDLER_H_
ConcreteHandler.h:
#ifndef CONCRETE_HANDLER_H_
#define CONCRETE_HANDLER_H_#include <string>
#include <cstdio>
#include "BaseHandler.h"// 具体处理者: 组长(仅处理面额<=10的票据)
class GroupLeader : public BaseApprover {public:explicit GroupLeader(std::string name) : BaseApprover(10, name) {}
};// 具体处理者: 经理(仅处理面额<=100的票据)
class Manager : public BaseApprover {public:explicit Manager(std::string name) : BaseApprover(100, name) {}
};// 具体处理者: 老板(仅处理面额<=1000的票据)
class Boss : public BaseApprover {public:explicit Boss(std::string name) : BaseApprover(1000, name) {}
};#endif // CONCRETE_HANDLER_H_
main.cpp:
#include "ConcreteHandler.h"int main() {// 请求处理者: 组长、经理和老板GroupLeader* group_leader = new GroupLeader("张组长");Manager* manager = new Manager("王经理");Boss* boss = new Boss("李老板");// 设置上级group_leader->setSuperior(manager);manager->setSuperior(boss);// 不同面额的票据统一先交给组长审批group_leader->handleRequest(8);group_leader->handleRequest(88);group_leader->handleRequest(888);group_leader->handleRequest(8888);delete group_leader;delete manager;delete boss;return 0;
}
输出结果:
张组长处理了该票据, 票据面额:8.000000
张组长无权处理, 转交上级...
王经理处理了该票据, 票据面额:88.000000
张组长无权处理, 转交上级...
王经理无权处理, 转交上级...
李老板处理了该票据, 票据面额:888.000000
张组长无权处理, 转交上级...
王经理无权处理, 转交上级...
无人有权限处理该票据, 票据金额:8888.000000
命令模式(Command)
前言
假如你正在开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编译器的不同操作。你创建了一个「Button」类。它不仅用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮。
尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开、保存、打印和应用等)。你会在哪里放置这些按钮的点击处理代码呢?最简单的解决方案是在使用按钮的每个地方都创建大量的子类。这些子类中包含按钮点击后必须执行的代码。
你很快就意识到这种方式有严重缺陷。首先,你创建了大量的子类,当每次修改基类 按钮 时,你都有可能需要修改所有子类的代码。简单来说,GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。
还有另外一个问题最难办。复制/粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上的“复制”按钮,或者通过右键菜单复制一些文字,又或者直接使用键盘上的Ctrl+C。此时你要么需要将操作代码复制进许多个类中,要么需要让右键菜单依赖于按钮,而后者是更糟糕的选择。
命令模式建议将请求的所有细节(例如调用的对象、方法名称和参数列表)抽取出来组成命令类,该类中仅包含一个触发请求的方法。
命令对象负责连接不同的 GUI 和业务逻辑对象。此后,GUI对象无需了解业务逻辑对象是否获得了请求,也无需了解其对请求进行处理的方式。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。
下一步是让所有命令实现相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。
应用命令模式后,我们不再需要任何按钮子类来实现点击行为。我们只需在「Button」基类中添加一个成员变量来存储对于命令对象的引用,并在点击后执行该命令即可。
你需要为每个可能的操作实现一系列命令类,并且根据按钮所需行为将命令和按钮连接起来。其他菜单、快捷方式或整个对话框等 GUI 元素都可以通过同方式来实现。当用户与 GUI 元素交互时,与其连接的命令将会被执行。现在你很可能已经猜到了,与相同操作相关的元素将会被连接到相同的命令,从而避免了重复代码。
实现结构
- 发送者(Sender)类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。注意,发送者并不负责创建命令对象,它通常会通过构造函数从客户端处获得预先生成的命令。
- 命令(Command)接口通常仅声明一个执行命令的方法。
- 具体命令(Concrete Commands)会实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。
- 接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。
- 客户端(Client)会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。
应用场景及优缺点
- 如果你需要通过操作来参数化对象,可使用命令模式。
命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:你可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。
- 如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。
同其他对象一样,命令也可以实现序列化(序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,你可以延迟或计划命令的执行。
优点:
- 单一职责原则。你可以解耦触发和执行操作的类。
- 开闭原则。你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 你可以实现撤销和恢复功能。
- 你可以实现操作的延迟执行。
- 你可以将一组简单命令组合成一个复杂命令。
缺点:代码可能会变得更加复杂,因为在发送者和接收者之间增 加了一个全新的层次。
实例演示
本例中,我们用遥控器(Controller)控制电视(TV)。
Invoker.h:
#ifndef INVOKER_H_
#define INVOKER_H_#include <memory>
#include "Command.h"// 触发者: 遥控器
class Controller{public:Controller() {}// 设置命令void setCommand(std::shared_ptr<Command> cmd) {cmd_ = cmd;}// 执行命令void executeCommand() {cmd_->execute();}private:std::shared_ptr<Command> cmd_;
};#endif // INVOKER_H_
Command.h:
#ifndef COMMAND_H_
#define COMMAND_H_// 命令接口, C++中为抽象基类
class Command {public:virtual void execute() = 0;
};#endif // COMMAND_H_
ConcreteCommand.h:
#ifndef CONCRETE_COMMAND_H_
#define CONCRETE_COMMAND_H_#include <memory>
#include "Command.h"
#include "Receiver.h"// 具体命令类: 打开电视
class TVOpenCommand : public Command{public:explicit TVOpenCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->open();}private:std::shared_ptr<Television> tv_;
};// 具体命令类: 关闭电视
class TVCloseCommand : public Command{public:explicit TVCloseCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->close();}private:std::shared_ptr<Television> tv_;
};// 具体命令类: 切换频道
class TVChangeCommand : public Command{public:explicit TVChangeCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->changeChannel();}private:std::shared_ptr<Television> tv_;
};#endif // CONCRETE_COMMAND_H_
Receiver.h:
#ifndef RECEIVER_H_
#define RECEIVER_H_#include <iostream>// 接受者: 电视
class Television{public:void open() {std::cout << "打开电视机!" << std::endl;}void close() {std::cout << "关闭电视机!" << std::endl;}void changeChannel(){std::cout << "切换电视频道!" << std::endl;}
};#endif // RECEIVER_H_
main.cpp:
#include "Invoker.h"
#include "ConcreteCommand.h"int main() {// 接收者: 电视机std::shared_ptr<Television> tv = std::make_shared<Television>();// 命令std::shared_ptr<Command> openCommand = std::make_shared<TVOpenCommand>(tv);std::shared_ptr<Command> closeCommand = std::make_shared<TVCloseCommand>(tv);std::shared_ptr<Command> changeCommand = std::make_shared<TVChangeCommand>(tv);// 调用者: 遥控器std::shared_ptr<Controller> controller = std::make_shared<Controller>();// 测试controller->setCommand(openCommand);controller->executeCommand();controller->setCommand(closeCommand);controller->executeCommand();controller->setCommand(changeCommand);controller->executeCommand();
}
输出结果
打开电视机!
关闭电视机!
切换电视频道!
迭代器模式(Iterator)
前言
大部分集合使用简单列表存储元素。但有些集合还会使用栈、树、图和其他复杂的数据结构。
无论集合的构成方式如何,它都必须提供某种访问元素的方式,便于其他代码使用其中的元素。集合应提供一种能够遍历元素的方式,且保证它不会周而复始地访问同一个元素。
如果你的集合基于列表, 那么这项工作听上去仿佛很简单。但如何遍历复杂数据结构(例如树)中的元素呢?例如,今天你需要使用深度优先算法来遍历树结构,明天可能会需要广度优先算法;下周则可能会需要其他方式(比如随机存取树中的元素)。
不断向集合中添加遍历算法会模糊其“高效存储数据”的主要职责。此外,有些算法可能是根据特定应用订制的,将其加入泛型集合类中会显得非常奇怪。
另一方面,使用多种集合的客户端代码可能并不关心存储数据的方式。不过由于集合提供不同的元素访问方式,你的代码将不得不与特定集合类进行耦合。
迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象。
除实现自身算法外, 迭代器还封装了遍历操作的所有细节,例如当前位置和末尾剩余元素的数量。因此,多个迭代器可以在相互独立的情况下同时访问集合。
迭代器通常会提供一个获取集合元素的基本方法。客户端可不断调用该方法直至它不返回任何内容,这意味着迭代器已经遍历了所有元素。
所有迭代器必须实现相同的接口。这样一来,只要有合适的迭代器, 客户端代码就能兼容任何类型的集合或遍历算法。如果你需要采用特殊方式来遍历集合,只需创建一个新的迭代器类即可,无需对集合或客户端进行修改。
实现结构
- 迭代器(Iterator):接口类,声明了遍历集合所需的操作(获取下一个元素、获取当前位置和重新开始迭代等)。
- 具体迭代器(Concrete Iterators) :实现遍历集合的一种特定算法。迭代器对象必须跟踪自身遍历的进度,这使得多个迭代器可以相互独立地遍历同一集合。
- 集合(Collection):接口类,声明一个或多个方法来获取与集合兼容的迭代器。请注意,返回方法的类型必须被声明为迭代器接口,因此具体集合可以返回各种不同种类的迭代器。
- 具体集合(Concrete Collections) :会在客户端请求迭代器时返回一个特定的具体迭代器类实体。你可能会琢磨,剩下的集合代码在什么地方呢? 不用担心, 它也会在同一个类中。只是这些细节对于实际模式来说并不重要,所以我们将其省略了而已。
- 客户端(Client):通过集合和迭代器的接口与两者进行交互。这样一来客户端无需与具体类进行耦合,允许同一客户端代码使用各种不同的集合和迭代器。客户端通常不会自行创建迭代器,而是会从集合中获取。但在特定情况下,客户端可以直接创建一个迭代器(例如当客户端需要自定义特殊迭代器时)。
应用场景及优缺点
- 当集合背后为复杂的数据结构,且你希望对客户端隐藏其复杂性时(出于使用便利性或安全性的考虑),可以使用迭代器模式。
迭代器封装了与复杂数据结构进行交互的细节,为客户端提供多个访问集合元素的简单方法。这种方式不仅对客户端来说非常方便,而且能避免客户端在直接与集合交互时执行错误或有害的操作,从而起到保护集合的作用。
- 使用该模式可以减少程序中重复的遍历代码。
重要迭代算法的代码往往体积非常庞大。当这些代码被放置在程序业务逻辑中时,它会让原始代码的职责模糊不清,降低其可维护性。因此,将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。
- 如果你希望代码能够遍历不同的甚至是无法预知的数据结构,可以使用迭代器模式。
该模式为集合和迭代器提供了一些通用接口。如果你在代码中使用了这些接口,那么将其他实现了这些接口的集合和迭代器传递给它时,它仍将可以正常运行。
优点:
- 单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类,你可对客户端代码和集合进行整理。
- 开闭原则。你可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码。
- 你可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态。
- 相似的,你可以暂停遍历并在需要时继续。
缺点:
- 如果你的程序只与简单的集合进行交互,应用该模式可能会矫枉过正。
- 对于某些特殊集合,使用迭代器可能比直接遍历的效率低。
实例演示
以下演示了使用迭代器模式来遍历电视频道列表的过程。
Iterator.h:
#ifndef ITERATOR_H_
#define ITERATOR_H_#include <string>// 抽象迭代器
class TVIterator{public:virtual void setChannel(int i) = 0;virtual void next() = 0;virtual void previous() = 0;virtual bool isLast() = 0;virtual std::string currentChannel() = 0;virtual bool isFirst() = 0;
};#endif // ITERATOR_H_
ConcreteIterator.h:
#ifndef CONCRETE_ITERATOR_H_
#define CONCRETE_ITERATOR_H_#include <string>
#include <vector>
#include "Iterator.h"// 具体迭代器
class SkyworthIterator : public TVIterator{public:explicit SkyworthIterator(std::vector<std::string> &tvs) : tvs_(tvs) {}void next() override {if (current_index_ < tvs_.size()) {current_index_++;}}void previous() override {if (current_index_ > 0) {current_index_--;}}void setChannel(int i) override {current_index_ = i;}std::string currentChannel() override {return tvs_[current_index_];}bool isLast() override {return current_index_ == tvs_.size();}bool isFirst() override {return current_index_ == 0;}private:std::vector<std::string> &tvs_;int current_index_ = 0;
};#endif // CONCRETE_ITERATOR_H_
Collection.h:
#ifndef COLLECTION_H_
#define COLLECTION_H_#include <memory>
#include "Iterator.h"// 抽象集合
class Television {public:virtual std::shared_ptr<TVIterator> createIterator() = 0;
};#endif // COLLECTION_H_
ConcreteCollection.h:
#ifndef CONCRETE_COLLECTION_H_
#define CONCRETE_COLLECTION_H_#include <vector>
#include <string>
#include <memory>
#include "Collection.h"
#include "ConcreteIterator.h"class SkyworthTelevision : public Television {public:std::shared_ptr<TVIterator> createIterator() {return std::make_shared<SkyworthIterator>(tvs_);}void addItem(std::string item) {tvs_.push_back(item);}private:std::vector<std::string> tvs_;
};#endif // CONCRETE_COLLECTION_H_
main.cpp:
#include <iostream>
#include "ConcreteCollection.h"int main() {SkyworthTelevision stv;// 按照顺序输出stv.addItem("CCTV-1");stv.addItem("CCTV-2");stv.addItem("CCTV-3");stv.addItem("CCTV-4");stv.addItem("CCTV-5");auto iter = stv.createIterator();while (!iter->isLast()) {std::cout << iter->currentChannel() << std::endl;iter->next();}return 0;
}
中介者模式(Mediator)
前言
假如你有一个创建和修改客户资料的对话框,它由各种控件组成, 例如文本框(TextField)、复选框(Checkbox) 和按钮(Button)等。
某些表单元素可能会直接进行互动。例如,选中“我有一只狗”复选框后可能会显示一个隐藏文本框用于输入狗狗的名字。另一个例子是提交按钮必须在保存数据前校验所有输入内容。
如果直接在表单元素代码中实现业务逻辑,你将很难在程序其他表单中复用这些元素类。例如,由于复选框类与狗狗的文本框相耦合,所以将无法在其他表单中使用它。你要么使用渲染资料表单时用到的所有类,要么一个都不用。
中介者模式建议你停止组件之间的直接交流并使其相互独立。这些组件必须调用特殊的中介者对象,通过中介者对象重定向调用行为,以间接的方式进行合作。最终,组件仅依赖于一个中介者类,无需与多个其他组件相耦合。
在资料编辑表单的例子中, 对话框(Dialog)类本身将作为中介者,其很可能已知自己所有的子元素,因此你甚至无需在该类中引入新的依赖关系。
绝大部分重要的修改都在实际表单元素中进行。让我们想想提交按钮。之前,当用户点击按钮后,它必须对所有表单元素数值进行校验。而现在它的唯一工作是将点击事件通知给对话框。收到通知后,对话框可以自行校验数值或将任务委派给各元素。这样一来, 按钮不再与多个表单元素相关联,而仅依赖于对话框类。
实现结构
- 组件(Component)是各种包含业务逻辑的类。每个组件都有一个指向中介者的引用,该引用被声明为中介者接口类型。组件不知道中介者实际所属的类,因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。
- 中介者(Mediator)接口声明了与组件交流的方法, 但通常仅包括一个通知方法。组件可将任意上下文(包括自己的对象)作为该方法的参数,只有这样接收组件和发送者类之间才不会耦合。
- 具体中介者(Concrete Mediator)封装了多种组件间的关系。具体中介者通常会保存所有组件的引用并对其进行管理,甚至有时会对其生命周期进行管理。
- 组件并不知道其他组件的情况。如果组件内发生了重要事件,它只能通知中介者。中介者收到通知后能轻易地确定发送者,这或许已足以判断接下来需要触发的组件了。对于组件来说,中介者看上去完全就是一个黑箱。发送者不知道最终会由谁来处理自己的请求,接收者也不知道最初是谁发出了请求。
应用场景及优缺点
- 当一些对象和其他对象紧密耦合以致难以对其进行修改时,可使用中介者模式。
该模式让你将对象间的所有关系抽取成为一个单独的类,以使对于特定组件的修改工作独立于其他组件。
- 当组件因过于依赖其他组件而无法在不同应用中复用时,可使用中介者模式。
应用中介者模式后,每个组件不再知晓其他组件的情况。尽管这些组件无法直接交流,但它们仍可通过中介者对象进行间接交流。如果你希望在不同应用中复用一个组件,则需要为其提供一个新的中介者类。
- 如果为了能在不同情景下复用一些基本行为,导致你需要被迫创建大量组件子类时,可使用中介者模式。
由于所有组件间关系都被包含在中介者中,因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。
优点:
- 单一职责原则。你可以将多个组件间的交流抽取到同一位置,使其更易于理解和维护。
- 开闭原则。你无需修改实际组件就能增加新的中介者。
- 你可以减轻应用中多个组件间的耦合情况。
- 你可以更方便地复用各个组件。
缺点:一段时间后,中介者可能会演化成为上帝对象。
实例演示
以下演示了中介者模式的一个实例,用于模拟房地产中介的业务逻辑。
Component.h:
#ifndef COMPONENT_H_
#define COMPONENT_H_#include "Mediator.h"
#include <cstdio>
#include <string>enum PERSON_TYPE {kUnknown,kLandlord,kTenant,
};// 组件基类
class Colleague {public:void set_mediator(Mediator *m) {mediator_ = m;}Mediator* get_mediator() {return mediator_;}void set_personType(PERSON_TYPE pt) {person_type_ = pt;}PERSON_TYPE get_person_type() {return person_type_;}virtual void ask() = 0;virtual void answer() = 0;private:Mediator* mediator_;PERSON_TYPE person_type_;
};// 具体组件1: 房东
class Landlord : public Colleague {public:Landlord() {name_ = "unknown";price_ = -1;address_ = "unknown";phone_number_ = "unknown";set_personType(kUnknown);}Landlord(std::string name, int price, std::string address, std::string phone_number) {name_ = name;price_ = price;address_ = address;phone_number_ = phone_number;set_personType(kLandlord);}void answer() override {printf("房东姓名:%s 房租:%d 地址:%s 电话:%s\n", name_.c_str(), price_, address_.c_str(), phone_number_.c_str());}void ask() override {printf("房东%s查看租客信息: \n", name_.c_str());this->get_mediator()->operation(this);}private:std::string name_;int price_;std::string address_;std::string phone_number_;
};// 具体组件2: 租客
class Tenant : public Colleague {public:Tenant() {name_ = "unknown";}explicit Tenant(std::string name) {name_ = name;set_personType(kTenant);}void ask() {printf("租客%s询问房东信息:\n", name_.c_str());this->get_mediator()->operation(this);}void answer() {printf("租客姓名: %s\n", name_.c_str());}private:std::string name_;
};#endif // COMPONENT_H_
Mediator.h:
#ifndef MEDIATOR_H_
#define MEDIATOR_H_#include <string>class Colleague;// 抽象中介者
class Mediator {public:// 声明抽象方法virtual void registerMethod(Colleague*) = 0;// 声明抽象方法virtual void operation(Colleague*) = 0;
};#endif // MEDIATOR_H_
ConcreteMediator.h:
#ifndef CONCRETE_MEDIATOR_H_
#define CONCRETE_MEDIATOR_H_#include <vector>
#include <string>
#include "Component.h"
#include "Mediator.h"// 具体中介类: 房产中介
class Agency : public Mediator {public:void registerMethod(Colleague* person) override {switch (person->get_person_type()) {case kLandlord:landlord_list_.push_back(reinterpret_cast<Landlord*>(person));break;case kTenant:tenant_list_.push_back(reinterpret_cast<Tenant*>(person));break;default:printf("wrong person\n");}}void operation(Colleague* person) {switch (person->get_person_type()) {case kLandlord:for (int i = 0; i < tenant_list_.size(); i++) {tenant_list_[i]->answer();}break;case kTenant:for (int i = 0; i < landlord_list_.size(); i++) {landlord_list_[i]->answer();}break;default:break;}}private:std::vector<Landlord*> landlord_list_;std::vector<Tenant*> tenant_list_;
};#endif // CONCRETE_MEDIATOR_H_
main.cpp:
#include <iostream>
#include "ConcreteMediator.h"
#include "Component.h"int main() {// 房产中介Agency *mediator = new Agency();// 三位房东Landlord *l1 = new Landlord("张三", 1820, "天津", "1333");Landlord *l2 = new Landlord("李四", 2311, "北京", "1555");Landlord *l3 = new Landlord("王五", 3422, "河北", "1777");l1->set_mediator(mediator);l2->set_mediator(mediator);l3->set_mediator(mediator);mediator->registerMethod(l1);mediator->registerMethod(l2);mediator->registerMethod(l3);// 两位租客Tenant *t1 = new Tenant("Zhang");Tenant *t2 = new Tenant("Yang");t1->set_mediator(mediator);t2->set_mediator(mediator);mediator->registerMethod(t1);mediator->registerMethod(t2);// 业务逻辑t1->ask();std::cout << std::endl;l1->ask();delete mediator;delete l1;delete l2;delete l3;delete t1;delete t2;
}
备忘录模式(Memento)
前言
假设你正在开发一款文字编辑器应用程序,除了简单的文字编辑功能外,编辑器中还要有设置文本格式和插入内嵌图片等功能。
后来你决定让用户能撤销施加在文本上的任何操作。你选择采用最直接的方式来实现该功能:程序在执行任何操作前会记录所有的对象状态, 并将其保存下来。当用户此后需要撤销某个操作时,程序将从历史记录中获取最近的快照,然后使用它来恢复所有对象的状态。
想要保存状态快照存在如下问题:
- 生成快照需要遍历对象的所有成员变量并将其数值复制保存,然而大部分对象会用私有成员变量来存储重要数据,这样别人就无法轻易查看其中的内容
- 即使对象的所有成员变量都是共有(public)的,未来添加或删除一些成员变量时需要对负责复制受影响对象状态的类进行更改
备忘录模式将创建状态快照(Snapshot)的工作委派给实际状态的拥有者原发器(Originator)对象。这样其他对象就不再需要从“外部”复制编辑器状态了,编辑器类拥有其状态的完全访问权,因此可以自行生成快照。
模式建议将对象状态的副本存储在一个名为备忘录(Memento)的特殊对象中。除了创建备忘录的对象外,任何对象都不能访问备忘录的内容。其他对象必须使用受限接口与备忘录进行交互,它们可以获取快照的元数据(创建时间和操作名称等),但不能获取快照中原始对象的状态。
在文字编辑器的示例中, 我们可以创建一个独立的历史(History)类作为负责人。编辑器每次执行操作前,存储在负责人中的备忘录栈都会生长。你甚至可以在应用的 UI 中渲染该栈,为用户显示之前的操作历史。
当用户触发撤销操作时,历史类将从栈中取回最近的备忘录,并将其传递给编辑器以请求进行回滚。由于编辑器拥有对备忘录的完全访问权限,因此它可以使用从备忘录中获取的数值来替换自身的状态。
实现结构
基于嵌套类的实现
- 原发器(Originator)类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。
- 备忘录 (Memento) 是原发器状态快照的值对象 (valueobject)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。
- 负责人(Caretaker)仅知道“何时”和“为何”捕捉原发器的状态,以及何时恢复状态。负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部的备忘录,并将其传递给原发器的恢复(restoration)方法。
- 在该实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法,即使这些方法被声明为私有。另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能在栈中保存备忘录,而不能修改其状态。
应用场景及优缺点
- 当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
备忘录模式允许你复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。尽管大部分人因为“撤 销”这个用例才记得该模式,但其实它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
- 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。
备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照,这有效地保障了数据的安全性。
优点:
- 你可以在不破坏对象封装情况的前提下创建对象状态快照。
- 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。
缺点:
- 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
- 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
实例演示
Memento.h:
#ifndef MEMENTO_H_
#define MEMENTO_H_#include <string>// 备忘录类保存编辑器的过往状态
class Snapshot {public:Snapshot(std::string text, int x, int y, double width): text_(text), cur_x_(x), cur_y_(y), selection_width_(width) {}std::string get_text() {return text_;}int get_cur_x() {return cur_x_;}int get_cur_y() {return cur_y_;}double get_selection_width() {return selection_width_;}private:const std::string text_;const int cur_x_;const int cur_y_;const double selection_width_;
};#endif // MEMENTO_H_
Originator.h:
#ifndef ORIGINATOR_H_
#define ORIGINATOR_H_#include <cstdio>
#include <string>
#include <memory>
#include "Memento.h"// 原发器中包含了一些可能会随时间变化的重要数据
// 它还定义了在备忘录中保存自身状态的方法, 以及从备忘录中恢复状态的方法
class Editor {public:void setText(std::string text) {text_ = text;}void setCursor(int x, int y) {cur_x_ = x;cur_y_ = y;}void setSelectionWidth(double width) {selection_width_ = width;}// 在备忘录中保存当前的状态std::shared_ptr<Snapshot> createSnapshot() {// 备忘录是不可变的对象, 因此原发器会将自身状态作为参数传递给备忘录的构造函数auto res = std::make_shared<Snapshot>(text_, cur_x_, cur_y_, selection_width_);printf("创建编辑器快照成功, text:%s x:%d y:%d width:%.2f\n", text_.c_str(), cur_x_, cur_y_, selection_width_);return res;}void resotre(std::shared_ptr<Snapshot> sptr_snapshot) {text_ = sptr_snapshot->get_text();cur_x_ = sptr_snapshot->get_cur_x();cur_y_ = sptr_snapshot->get_cur_y();selection_width_ = sptr_snapshot->get_selection_width();printf("恢复编辑器状态成功, text:%s x:%d y:%d width:%.2f\n", text_.c_str(), cur_x_, cur_y_, selection_width_);}private:// 文本std::string text_;// 光标位置int cur_x_;int cur_y_;// 当前滚动条位置double selection_width_;
};#endif // ORIGINATOR_H_
Caretaker.h:
#ifndef CARETAKER_H_
#define CARETAKER_H_#include <memory>
#include "Memento.h"
#include "Originator.h"class Command {public:explicit Command(Editor* e) : editor_(e) {}void makeBackup() {backup_ = editor_->createSnapshot();}void undo() {if (backup_) {editor_->resotre(backup_);}}private:Editor *editor_;std::shared_ptr<Snapshot> backup_;
};#endif // CARETAKER_H_
main.cpp:
#include "Caretaker.h"int main() {// 创建原发器和负责人Editor editor;Command command(&editor);// 定义初始状态editor.setText("TOMOCAT");editor.setCursor(21, 34);editor.setSelectionWidth(3.4);// 保存状态command.makeBackup();// 更改编辑器状态editor.setText("KKKK");editor.setCursor(111, 222);editor.setSelectionWidth(111.222);// 撤销command.undo();return 0;
}
输出结果
创建编辑器快照成功, text:TOMOCAT x:21 y:34 width:3.40
恢复编辑器状态成功, text:TOMOCAT x:21 y:34 width:3.40
观察者模式(Observer)
前言
假如你有两种类型的对象:“顾客”和“商店”。顾客对某个特定品牌的产品非常感兴趣(例如最新型号的 iPhone 手机),而该产品很快将会在商店里出售。
一方面顾客可以每天来商店看看产品是否到货。但如果商品尚未到货时,绝大多数来到商店的顾客都会空手而归。
另一方面,每次新产品到货时,商店可以向所有顾客发送邮件(可能会被视为垃圾邮件)。这样,部分顾客就无需反复前往商店了,但也可能会惹恼对新产品没有兴趣的其他顾客。
我们似乎遇到了一个矛盾:要么让顾客浪费时间检查产品是否到货,要么让商店浪费资源去通知没有需求的顾客。
拥有一些值得关注的状态的对象通常被称为目标,由于它要将自身的状态改变通知给其他对象,我们也将其称为发布者(publisher)。所有希望关注发布者状态变化的其他对象被称为订阅者(subscribers)。
观察者模式建议你为发布者类添加订阅机制,让每个对象都能订阅或取消订阅发布者事件流。实际上,该机制包括:
- 一个用于存储订阅者对象引用的列表成员变量。
- 几个用于添加或删除该列表中订阅者的公有方法。
实现结构
- 发布者(Publisher)会向其他对象发送值得关注的事件。事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。
- 当新事件发生时,发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。
- 订阅者(Subscriber)接口声明了通知接口。 在绝大多数情况下,该接口仅包含一个 update方法。该方法可以拥有多个参数,使发布者能在更新时传递事件的详细信息。
- 具体订阅者(Concrete Subscribers)可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口,因此发布者不需要与具体类相耦合。
- 订阅者通常需要一些上下文信息来正确地处理更新。 因此,发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递,使订阅者直接获取所需的数据。
- 客户端(Client)会分别创建发布者和订阅者对象,然后为订阅者注册发布者更新。
应用场景及优缺点
- 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。
当你使用图形用户界面类时通常会遇到一个问题。比如,你创建了自定义按钮类并允许客户端在按钮中注入自定义代码,这样当用户按下按钮时就会触发这些代码。观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。你可在按钮中添加订阅机制,允许客户端通过自定义订阅类注入自定义代码。
- 当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。
订阅列表是动态的,因此订阅者可随时加入或离开该列表。
优点:
- 开闭原则。 你无需修改发布者代码就能引入新的订阅者类(如果是发布者接口则可轻松引入发布者类)。
- 你可以在运行时建立对象之间的联系。
缺点:订阅者的通知顺序是随机的。
实例演示
Publisher.h:
#ifndef PUBLISHER_H_
#define PUBLISHER_H_#include <vector>
#include <iostream>
#include "Subscriber.h"class Cat {public:// 注册观察者void attach(AbstractObserver* observer) {observers_.push_back(observer);}// 注销观察者void detach(AbstractObserver* observer) {for (auto it = observers_.begin(); it !=observers_.end(); it++) {if (*it == observer) {observers_.erase(it);break;}}}void cry() {std::cout << "猫叫!" << std::endl;for (auto ob : observers_) {ob->response();}}private:std::vector<AbstractObserver*> observers_;
};#endif // PUBLISHER_H_
Subscriber.h:
#ifndef SUBSCRIBER_H_
#define SUBSCRIBER_H_class AbstractObserver {public:virtual void response() = 0;
};#endif // SUBSCRIBER_H_
ConcreteSubscriber.h:
#ifndef CONCRETE_SUBSCRIBER_H_
#define CONCRETE_SUBSCRIBER_H_#include <iostream>
#include "Subscriber.h"// 具体观察者1: 老鼠
class Mouse : public AbstractObserver {public:void response() override {std::cout << "老鼠逃跑" << std::endl;}
};// 具体观察者2: 狗
class Dog : public AbstractObserver {public:void response() override {std::cout << "狗追猫" << std::endl;}
};#endif // CONCRETE_SUBSCRIBER_H_
main.cpp:
#include "Publisher.h"
#include "ConcreteSubscriber.h"int main() {// 发布者Cat cat;// 观察者Mouse mouse;Dog dog;// 添加订阅关系cat.attach(&mouse);cat.attach(&dog);// 发布消息cat.cry();return 0;
}
输出结果
猫叫!
老鼠逃跑
狗追猫
状态模式(State)
前言
状态模式与有限状态机的概念紧密相关。
其主要思想是程序在任意时刻仅可处于几种有限的状态中。在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态。不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变。这些数量有限且预先定义的状态切换规则被称为转移。
你还可将该方法应用在对象上。 假如你有一个文档(Document)类,文档可能处于草稿(Draft)、审阅中(Moderation)和已发布(Published)三种状态中的一种。文档的publish方法在不同状态下的行为略有不同:
- 处于草稿状态时,它会将文档转移到审阅中状态。
- 处于审阅中状态时,如果当前用户是管理员,它会公开发布文档。
- 处于已发布状态时,它不会进行任何操作。
状态机通常由众多条件运算符( if 或 switch )实现,可根据对象的当前状态选择相应的行为。“状态”通常只是对象中的一组成员变量值。但随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻。
状态模式建议为对象的所有可能状态新建一个类,然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文(context),它并不会自行实现所有行为,而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象。
如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。
这个结构可能看上去与策略模式相似,但有一个关键性的不同——在状态模式中, 特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换;策略则几乎完全不知道其他策略的存在。
实现结构
- 上下文(Context)保存了对于一个具体状态对象的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互,且会提供一个设置器用于传递新的状态对象。
- 状态(State)接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解,因为你不希望某些状态所拥有的方法永远不会被调用。
- 具体状态(Concrete States)会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。状态可以通过
该引用从上下文处获取所需信息,并且能触发状态转移。 - 上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。
应用场景及优缺点
- 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。这样一来,你可以在独立于其他状态的情况下添加新状态或修改已有状态,从而减少维护成本。
- 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。同时,你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
优点:
- 单一职责原则。将与特定状态相关的代码放在单独的类中。
- 开闭原则。无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
缺点:如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。
实例演示
Context.h:
#ifndef CONTEXT_H_
#define CONTEXT_H_#include <string>
#include <memory>class AbstractState;// 论坛账号
class ForumAccount {public:explicit ForumAccount(std::string name);void set_state(std::shared_ptr<AbstractState> state) {state_ = state;}std::shared_ptr<AbstractState> get_state() {return state_;}std::string get_name() {return name_;}void downloadFile(int score);void writeNote(int score);void replyNote(int score);private:std::shared_ptr<AbstractState> state_;std::string name_;
};#endif // CONTEXT_H_
Context.cpp:
#include "Context.h"#include "ConcreteState.h"
#include <string>ForumAccount::ForumAccount(std::string name): name_(name), state_(std::make_shared<PrimaryState>(this)) {printf("账号%s注册成功!\n", name.c_str());
}void ForumAccount::downloadFile(int score) {state_->downloadFile(score);
}void ForumAccount::writeNote(int score) {state_->writeNote(score);
}void ForumAccount::replyNote(int score) {state_->replyNote(score);
}
State.h:
#ifndef STATE_H_
#define STATE_H_#include <string>
#include <cstdio>
#include "Context.h"class AbstractState {public:virtual void checkState() = 0;void set_point(int point) {point_ = point;}int get_point() {return point_;}void set_state_name(std::string name) {state_name_ = name;}std::string get_state_name() {return state_name_;}ForumAccount* get_account() {return account_;}virtual void downloadFile(int score) {printf("%s下载文件, 扣除%d积分。\n", account_->get_name().c_str(), score);point_ -= score;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}virtual void writeNote(int score) {printf("%s发布留言, 增加%d积分。\n", account_->get_name().c_str(), score);point_ += score;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}virtual void replyNote(int score) {printf("%s回复留言, 增加%d积分。\n", account_->get_name().c_str(), score);point_ += score;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}protected:ForumAccount* account_;int point_;std::string state_name_;
};#endif // STATE_H_
ConcreteState.h:
#ifndef CONCRETE_STATE_H_
#define CONCRETE_STATE_H_#include <cstdio>
#include "State.h"// 具体状态类: 新手
class PrimaryState : public AbstractState {public:explicit PrimaryState(AbstractState* state) {account_ = state->get_account();point_ = state->get_point();state_name_ = "新手";}explicit PrimaryState(ForumAccount *account) {account_ = account;point_ = 0;state_name_ = "新手";}void downloadFile(int score) override {printf("对不起, %s没有下载文件的权限!\n", account_->get_name().c_str());}void checkState() override;
};// 具体状态类: 高手
class MiddleState : public AbstractState {public:explicit MiddleState(AbstractState* state) {account_ = state->get_account();point_ = state->get_point();state_name_ = "高手";}void writeNote(int score) override {printf("%s发布留言, 增加%d积分。\n", account_->get_name().c_str(), score * 2);point_ += score * 2;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}void checkState() override;
};// 具体状态类: 专家
class HighState : public AbstractState {public:explicit HighState(AbstractState* state) {account_ = state->get_account();point_ = state->get_point();state_name_ = "专家";}void writeNote(int score) override {printf("%s发布留言, 增加%d积分。\n", account_->get_name().c_str(), score * 2);point_ += score * 2;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}virtual void downloadFile(int score) {printf("%s下载文件, 扣除%d积分。\n", account_->get_name().c_str(), score / 2);point_ -= score / 2;checkState();printf("%s剩余积分为%d, 当前级别为%s。\n", account_->get_name().c_str(), point_, account_->get_state()->get_state_name().c_str());}void checkState() override;
};#endif // CONCRETE_STATE_H_
ConcreteState.cpp:
#include "ConcreteState.h"#include <memory>void PrimaryState::checkState() {if (point_ >= 1000) {account_->set_state(std::make_shared<HighState>(this));} else if (point_ >= 100) {account_->set_state(std::make_shared<MiddleState>(this));}
}void MiddleState::checkState() {if (point_ >= 1000) {account_->set_state(std::make_shared<HighState>(this));} else if (point_ < 100) {account_->set_state(std::make_shared<PrimaryState>(this));}
}void HighState::checkState() {if (point_ < 100) {account_->set_state(std::make_shared<PrimaryState>(this));} else if (point_ < 1000) {account_->set_state(std::make_shared<HighState>(this));}
}
main.cpp:
#include "Context.h"int main() {// 注册新用户ForumAccount account("TOMOCAT");account.writeNote(20);account.downloadFile(20);account.replyNote(100);account.writeNote(40);account.downloadFile(80);account.writeNote(1000);account.downloadFile(80);return 0;
}
输出结果
账号TOMOCAT注册成功!
TOMOCAT发布留言, 增加20积分。
TOMOCAT剩余积分为20, 当前级别为新手。
对不起, TOMOCAT没有下载文件的权限!
TOMOCAT回复留言, 增加100积分。
TOMOCAT剩余积分为120, 当前级别为高手。
TOMOCAT发布留言, 增加80积分。
TOMOCAT剩余积分为200, 当前级别为高手。
TOMOCAT下载文件, 扣除80积分。
TOMOCAT剩余积分为120, 当前级别为高手。
TOMOCAT发布留言, 增加2000积分。
TOMOCAT剩余积分为2120, 当前级别为专家。
TOMOCAT下载文件, 扣除40积分。
TOMOCAT剩余积分为2080, 当前级别为专家。
策略模式(Strategy)
前言
你打算为游客们创建一款导游程序。该程序的核心功能是提供美观的地图,以帮助用户在任何城市中快速定位。
用户期待的程序新功能是自动路线规划:他们希望输入地址后就能在地图上看到前往目的地的最快路线。
程序的首个版本只能规划公路路线。驾车旅行的人们对此非常满意。但很显然,并非所有人都会在度假时开车。因此你在下次更新时添加了规划步行路线的功能。此后,你又添加了规划公共交通路线的功能。
而这只是个开始。不久后,你又要为骑行者规划路线。又过了一段时间,你又要为游览城市中的所有景点规划路线。
尽管从商业角度来看,这款应用非常成功,但其技术部分却让你非常头疼:每次添加新的路线规划算法后,导游应用中主要类的体积就会增加一倍。终于在某个时候,你觉得自己没法继续维护这堆代码了。
无论是修复简单缺陷还是微调街道权重,对某个算法进行任何修改都会影响整个类,从而增加在已有正常运行代码中引入错误的风险。
此外,团队合作将变得低效。如果你在应用成功发布后招募了团队成员,他们会抱怨在合并冲突的工作上花费了太多时间。在实现新功能的过程中,你的团队需要修改同一个巨大的类,这样他们所编写的代码相互之间就可能会出现冲突。
策略模式建议找出负责用许多不同方式完成特定任务的类,然后将其中的算法抽取到一组被称为策略的独立类中。
名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用。上下文并不执行任务,而是将工作委派给已连接的策略对象。
上下文不负责选择符合任务需要的算法——客户端会将所需策略传递给上下文。实际上,上下文并不十分了解策略,它会通过同样的通用接口与所有策略进行交互,而该接口只需暴露一个方法来触发所选策略中封装的算法即可。
因此,上下文可独立于具体策略。这样你就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。
在导游应用中, 每个路线规划算法都可被抽取到只有一个buildRoute方法的独立类中。该方法接收起点和终点作为参数,并返回路线中途点的集合。
实现结构
- 上下文(Context)维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。
- 策略(Strategy)接口是所有具体策略的通用接口,它声明了一个上下文用于执行策略的方法。
- 具体策略(Concrete Strategies)实现了上下文所用算法的各种不同变体。
- 当上下文需要运行算法时,它会在其已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。
- 客户端(Client)会创建一个特定策略对象并将其传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。
应用场景及优缺点
- 当你想使用对象中各种不同的算法变体,并希望能在运行时切换算法时,可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象,从而以间接方式在运行时更改对象行为。
- 当你有许多仅在执行某些行为时略有不同的相似类时,可使用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中,并将原始类组合成同一个,从而减少重复代码。
- 如果算法在上下文的逻辑中不是特别重要,使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、内部数据和依赖关系与其他代码隔离开来。不同客户端可通过一个简单接口执行算法,并能在运行时进行切换。
优点:
- 你可以在运行时切换对象内的算法。
- 你可以将算法的实现和使用算法的代码隔离开来。
- 你可以使用组合来代替继承。
- 开闭原则。你无需对上下文进行修改就能够引入新的策略。
缺点:
- 如果你的算法极少发生改变,那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。
- 客户端必须知晓策略间的不同——它需要选择合适的策略。
- 许多现代编程语言支持函数类型功能,允许你在一组匿名函数中实现不同版本的算法。这样,你使用这些函数的方式就和使用策略对象时完全相同,无需借助额外的类和接口来保持代码简洁
实例演示
Strategy.h:
#ifndef STRATEGY_H_
#define STRATEGY_H_#include <vector>// 抽象策略类: 排序
class Sort {public:virtual void sortVector(std::vector<int> &arr) = 0;
};#endif // STRATEGY_H_
ConcreteStrategy.h:
#ifndef CONCRETE_STRATEGY_H_
#define CONCRETE_STRATEGY_H_#include <vector>
#include <string>
#include <utility>
#include <iostream>
#include "Strategy.h"// 打印vector内容
void printVector(const std::string prefix, const std::vector<int> &vi) {std::cout << prefix;for (auto i : vi) {std::cout << " " << i;}std::cout << std::endl;
}// 具体策略类: 冒泡排序
class BubbleSort : public Sort {public:void sortVector(std::vector<int> &vi) override {printVector("冒泡排序前:", vi);int len = vi.size();// 轮次: 从1到n-1轮for (int i = 0; i < len - 1; ++i) {// 优化: 判断本轮是否有交换元素, 如果没交换则可直接退出bool is_exchange = false;for (int j = 0; j < len - i - 1; ++j) {if (vi[j] > vi[j+1]) {std::swap(vi[j], vi[j+1]);is_exchange = true;}}// 如果本轮无交换, 则可以直接退出if (!is_exchange) {printVector("冒泡排序后:", vi);return;}}printVector("冒泡排序后:", vi);}
};// 具体策略类: 选择排序
class SelectionSort : public Sort {public:void sortVector(std::vector<int> &vi) override {printVector("选择排序前:", vi);// 需要进行 n-1 轮for (int i = 0; i < vi.size() - 1; ++i) {// 找到此轮的最小值下标int min_index = i;for (int j = i + 1; j < vi.size(); ++j) {if (vi[j] < vi[min_index]) {min_index = j;}}std::swap(vi[i], vi[min_index]);}printVector("选择排序后:", vi);}
};// 具体策略类: 插入排序
class InsertionSort : public Sort {public:void sortVector(std::vector<int> &vi) override {printVector("插入排序前:", vi);// 第一轮不需要操作, 第二轮比较一次, 第n轮比较 n-1 次for (int i = 1; i < vi.size(); ++i) {// 存储待插入的值和下标int insert_value = vi[i];int j = i - 1;while (j >= 0 && vi[j] > insert_value) {vi[j + 1] = vi[j]; // 如果左侧的已排序元素比目标值大, 那么右移j--;}// 注意这里insert_index 需要+1vi[j + 1] = insert_value;}printVector("插入排序后:", vi);}
};#endif // CONCRETE_STRATEGY_H_
Context.h:
#ifndef CONTEXT_H_
#define CONTEXT_H_#include <vector>
#include "Strategy.h"class ArrayHandler {public:void sortVector(std::vector<int> &arr) {return sort_->sortVector(arr);}void setSortStrategy(Sort* sort) {sort_ = sort;}private:Sort *sort_;
};#endif // CONTEXT_H_
main.cpp:
#include <vector>
#include <algorithm>
#include <random>
#include <iostream>
#include "ConcreteStrategy.h"
#include "Context.h"std::vector<int> test_array = {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};int main() {ArrayHandler array_handler;{// 冒泡排序 这里可以选择新策略BubbleSort* bubble_sort = new BubbleSort();auto rng = std::default_random_engine {};std::shuffle(std::begin(test_array), std::end(test_array), rng);array_handler.setSortStrategy(bubble_sort);array_handler.sortVector(test_array);delete bubble_sort;}return 0;
}
模板方法模式(Template Method)
前言
假如你正在开发一款分析公司文档的数据挖掘程序。用户需要向程序输入各种格式(PDF、DOC 或 CSV)的文档,程序则会试图从这些文件中抽取有意义的数据,并以统一的格式将其返回给用户。
该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中,程序能够支持 CSV 文件。一个月后,你“教会”了程序从 PDF 文件中抽取数据。
一段时间后,你发现这三个类中包含许多相似代码。尽管这些类处理不同数据格式的代码完全不同,但数据处理和分析的代码却几乎完全一样。
还有另一个与使用这些类的客户端代码相关的问题:客户端代码中包含许多条件语句,以根据不同的处理对象类型选择合适的处理过程。如果所有处理数据的类都拥有相同的接口或基类,那么你就可以去除客户端代码中的条件语句,转而使用多态机制来在处理对象上调用函数。
模板方法模式建议将算法分解为一系列步骤,然后将这些步骤改写为方法, 最后在“模板方法”中依次调用这些方法。为了能够使用算法,客户端需要自行提供子类并实现所有的抽象步骤。
让我们考虑如何在数据挖掘应用中实现上述方案。我们可为图中的三个解析算法创建一个基类,该类将定义调用了一系列不同文档处理步骤的模板方法。
首先,我们将所有步骤声明为“抽象”类型,强制要求子类自行实现这些方法。在我们的例子中,子类中已有所有必要的实现,因此我们只需调整这些方法的签名,使之与超类的方法匹配即可。
对于不同的数据格式,打开和关闭文件以及抽取和解析数据的代码都不同,因此无需修改这些方法。但分析原始数据和生成报告等其他步骤的实现方式非常相似,因此可将其提取到基类中,以让子类共享这些代码。
实现结构
- 抽象类(AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为“抽象”类型,也可以提供一些默认实现。
- 具体类(ConcreteClass)可以重写所有步骤,但不能重写模板方法自身。
应用场景及优缺点
- 当你只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤,以便子类能对其进行扩展,同时还可让超类中所定义的结构保持完整。
- 当多个类的算法除一些细微不同之外几乎完全一样时,你可使用该模式。但其后果就是,只要算法发生变化,你就可能需要修改所有的类。
在将算法转换为模板方法时,你可将相似的实现步骤提取到超类中以去除重复代码。子类间各不同的代码可继续保留在子类中。
优点:
- 你可仅允许客户端重写一个大型算法中的特定部分,使得算法其他部分修改对其所造成的影响减小。
- 你可将重复代码提取到一个超类中。
缺点:
- 部分客户端可能会受到算法框架的限制。
- 通过子类抑制默认步骤实现可能会导致违反里氏替换原则。
- 模板方法中的步骤越多,其维护工作就可能会越困难。
实例演示
AbstractClass.h:
#ifndef ABSTRACT_METHOD_H_
#define ABSTRACT_METHOD_H_#include <iostream>// 抽象类: 银行业务办理流程
class BankTemplateMethod {public:void takeNumber() {std::cout << "排队取号。" << std::endl;}virtual void transact() = 0;void evalute() {std::cout << "反馈评分。" << std::endl;}void process() {takeNumber();transact();evalute();}
};#endif // ABSTRACT_METHOD_H_
ConcreteClass.h:
#ifndef CONCRETE_METHOD_H_
#define CONCRETE_METHOD_H_#include "AbstractClass.h"// 具体类: 存款
class Deposit : public BankTemplateMethod {public:void transact() override {std::cout << "存款..." << std::endl;}
};// 具体类: 取款
class Withdraw : public BankTemplateMethod {public:void transact() override {std::cout << "取款..." << std::endl;}
};// 具体类: 转账
class Transfer : public BankTemplateMethod {public:void transact() override {std::cout << "转账..." << std::endl;}
};#endif // CONCRETE_METHOD_H_
main.cpp:
#include "ConcreteClass.h"int main() {// 存款BankTemplateMethod* deposit = new Deposit();deposit->process();delete deposit;// 取款BankTemplateMethod* withdraw = new Withdraw();withdraw->process();delete withdraw;// 转账BankTemplateMethod* transfer = new Transfer();transfer->process();delete transfer;
}
输出结果
排队取号。
存款...
反馈评分。
排队取号。
取款...
反馈评分。
排队取号。
转账...
反馈评分。
访问者模式(Vistor)
前言
假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市), 也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
一段时间后, 你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。
但你不太走运,系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
此外,他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。
还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。
访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
它使用了一种名为双分派的技巧,不使用累赘的条件语句也可下执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会“接收”一个访问者并告诉其应执行的访问者方法。
// 客户端代码
for (auto node : graph) {node->accpet(exportVistor);
}// 城市
class City {public:void accept(Vistor *v) {v->doForCity(this);}// ...
};// 工业区
class Industry {public:void accept(Vistor *v) {v->doForIndustry(this);}// ...
};
最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。
现在,如果抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。
实现结构
- 访问者(Visitor)接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
- 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。
- 元素(Element) 接口声明了一个方法来“接收” 访问者。该方法必须有一个参数被声明为访问者接口类型。
- 具体元素(Concrete Element)必须实现接收方法。 该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
- 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。 客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。
应用场景及优缺点
- 如果你需要对一个复杂对象结构(例如对象树)中的所有元素执行某些操作,可使用访问者模式。
访问者模式通过在访问者对象中为多个目标类提供相同操作的变体,让你能在属于不同类的一组对象上执行同一操作。
- 可使用访问者模式来清理辅助行为的业务逻辑。
该模式会将所有非主要的行为抽取到一组访问者类中,使得程序的主要类能更专注于主要的工作。
优点:
- 开闭原则。你可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。
- 单一职责原则。可将同一行为的不同版本移到同一个类中。
- 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。
缺点:
- 每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。
- 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。
实例演示
Visitor.h:
#ifndef VISTOR_H_
#define VISTOR_H_#include <string>class Apple;
class Book;// 抽象访问者
class Vistor {public:void set_name(std::string name) {name_ = name;}virtual void visit(Apple *apple) = 0;virtual void visit(Book *book) = 0;protected:std::string name_;
};#endif // VISTOR_H_
ConcreteVisitor.h:
#ifndef CONCRETE_VISTOR_H_
#define CONCRETE_VISTOR_H_#include <iostream>
#include "Visitor.h"// 具体访问者类: 顾客
class Customer : public Vistor {public:void visit(Apple *apple) {std::cout << "顾客" << name_ << "挑选苹果。" << std::endl;}void visit(Book *book) {std::cout << "顾客" << name_ << "买书。" << std::endl;}
};// 具体访问者类: 收银员
class Saler : public Vistor {public:void visit(Apple *apple) {std::cout << "收银员" << name_ << "给苹果过称, 然后计算价格。" << std::endl;}void visit(Book *book) {std::cout << "收银员" << name_ << "计算书的价格。" << std::endl;}
};#endif // CONCRETE_VISTOR_H_
Element.h:
#ifndef ELEMENT_H_
#define ELEMENT_H_#include "Visitor.h"// 抽象元素类
class Product {public:virtual void accept(Vistor *vistor) = 0;
};#endif // ELEMENT_H_
ConcreteElement.h:
#ifndef CONCRETE_ELEMENT_H_
#define CONCRETE_ELEMENT_H_#include "Element.h"// 具体产品类: 苹果
class Apple : public Product {public:void accept(Vistor *vistor) override {vistor->visit(this);}
};// 具体产品类: 书籍
class Book : public Product {public:void accept(Vistor *vistor) override {vistor->visit(this);}
};#endif // CONCRETE_ELEMENT_H_
Client.h:
#ifndef CLIENT_H_
#define CLIENT_H_#include <list>
#include "Visitor.h"
#include "Element.h"// 购物车
class ShoppingCart {public:void accept(Vistor *vistor) {for (auto prd : prd_list_) {prd->accept(vistor);}}void addProduct(Product *product) {prd_list_.push_back(product);}void removeProduct(Product *product) {prd_list_.remove(product);}private:std::list<Product*> prd_list_;
};#endif // CLIENT_H_
main.cpp:
#include "Client.h"
#include "ConcreteElement.h"
#include "ConcreteVisitor.h"int main() {Book book;Apple apple;ShoppingCart basket;basket.addProduct(&book);basket.addProduct(&apple);Customer customer;customer.set_name("小张");basket.accept(&customer);Saler saler;saler.set_name("小杨");basket.accept(&saler);return 0;
}
输出结果
顾客小张买书。
顾客小张挑选苹果。
收银员小杨计算书的价格。
收银员小杨给苹果过称, 然后计算价格。