本文是学习笔记,如有侵权,请联系删除。
参考链接
Youtube: C++设计模式
Gtihub源码与PPT:https://github.com/ZachL1/Bilibili-plus
豆瓣: 设计模式–可复用面向对象软件的基础
以下是23种设计模式的简要定义以及现实中的例子:
设计模式 | 定义 | 现实中的例子 |
---|---|---|
单例模式 (Singleton) | 保证一个类仅有一个实例,并提供一个全局访问点。 | 数据库连接池、日志对象、配置管理器等。 |
工厂方法模式 (Factory Method) | 定义一个创建对象的接口,但让子类决定实例化哪个类。 | STL中的std::vector 和std::list ,由不同的工厂方法创建。 |
抽象工厂模式 (Abstract Factory) | 提供一个创建一系列相关或相互依赖对象的接口。 | GUI库中的跨平台风格工厂,可以在不同平台上创建相应的按钮、文本框等。 |
建造者模式 (Builder) | 将一个复杂对象的构建与它的表示分离。 | 使用Builder构建器构建HTML文档,例如StringBuilder、DocumentBuilder等。 |
原型模式 (Prototype) | 通过复制现有对象来创建新对象,而不是通过实例化。 | 图形设计软件中的复制粘贴操作,以及版本控制系统中的分支和合并。 |
适配器模式 (Adapter) | 将一个类的接口转换成客户希望的另外一个接口。 | 将新的API适配到旧的API,例如多种设备的充电器适配器、各种文件格式的转换器等。 |
桥接模式 (Bridge) | 将抽象部分与实现部分分离,使它们都可以独立变化。 | GUI库中的窗口系统和图形系统之间的桥接,例如使用OpenGL和DirectX的图形引擎。 |
装饰者模式 (Decorator) | 动态地给一个对象添加一些额外的职责。 | Java I/O库中的BufferedReader 和BufferedWriter ,它们可以动态地为输入输出流添加缓冲区和其他功能。 |
组合模式 (Composite) | 将对象组合成树形结构以表示"部分-整体"的层次结构。 | GUI界面中的UI元素,文件系统中的目录和文件结构。 |
外观模式 (Facade) | 为子系统中的一组接口提供一个一致的界面。 | 操作系统中的系统调用接口,为用户提供对硬件和其他底层功能的简化访问。 |
享元模式 (Flyweight) | 通过共享实例来有效地支持大量细粒度对象。 | 文本编辑器中的字符对象、图形系统中的颜色对象等。 |
代理模式 (Proxy) | 为其他对象提供一种代理以控制对这个对象的访问。 | 远程代理、虚拟代理、保护代理等,例如Java的RMI(远程方法调用)中的远程代理。 |
责任链模式 (Chain of Responsibility) | 为解除发送者和接收者之间的耦合,而使多个对象都有机会处理请求。 | Java中的异常处理机制,一个异常对象被依次传递给多个异常处理器直至找到合适的处理器。 |
命令模式 (Command) | 将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。 | 遥控器中的按钮对象,每个按钮对象封装了一个具体的命令对象,通过按钮执行相应的命令。 |
策略模式 (Strategy) | 定义算法家族,分别封装起来,让它们之间可以相互替换。 | 排序算法、图像处理算法等,用户可以选择不同的算法实现。 |
状态模式 (State) | 允许一个对象在其内部状态改变时改变其行为。 | 电梯系统中的状态,例如电梯的开启、关闭、运行等状态。 |
观察者模式 (Observer) | 定义对象间的一种一对多的依赖关系,以便当一个对象状态改变时,所有依赖于它的对象都得到通知并被自动刷新。 | 消息订阅系统、GUI界面中的事件监听器等。 |
备忘录模式 (Memento) | 不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 | 文本编辑器中的撤销和恢复功能,版本控制系统中的快照和回滚功能。 |
迭代器模式 (Iterator) | 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露其内部的表示。 | STL中的迭代器,Java中的集合类的迭代器,文件系统中的目录遍历等。 |
访问者模式 (Visitor) | 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 | 编译器中的语法树遍历、文件系统中的文件访问等。 |
中介者模式 (Mediator) | 用一个中介对象来封装一系列的对象交互,使各对象不需要显式地相互引用。 | 聊天室中的聊天对象、飞机的交通管制系统等。 |
模板方法模式 (Template Method) | 定义一个操作中的算法的框架,而将一些步骤延迟到子类中。 | Java中的Collections.sort() ,算法的骨架定义在父类中,具体排序逻辑由子类实现。 |
解释器模式 (Interpreter) | 给定一个语言的文法和一个解释器,定义该语言中的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 | 编译器中的解释器,数据库查询语言中的解释器等。 |
这些例子旨在展示设计模式在不同领域和场景中的应用。设计模式的选择取决于具体问题的需求和上下文。
本文主要介绍Composite, Iterator, 职责链, Command, Visitor, Interpreter模式。
“数据结构”模式
常常有一些组件在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,将极大地破坏组件的复用。这时候,将这些特定数据结构封装在内部,在外部提供统一的接囗,来实现与特定数据结构无关的访问,是一种行之有效的解决方案。
典型模式
·Composite
·Iterator
·Chain of Resposibility
在软件在某些情况下,客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
如何将“客户代码与复杂的对象容器结构”解耦?让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一 样来处理复杂的对象容器?
在绘图编辑器和图形捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建复杂的图表。用户可以组合多个简单组件以形成一些较大的组件,这些组件又可以组合成更大的组件。一个简单的实现方法是为Text和Line这样的图元定义一些类,另外定义一些类作为这些图元的容器类(Container)。
然而这种方法存在一个问题:使用这些类的代码必须区别对待图元对象与容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂。Composite模式描述了如何使用递归组合,使得用户不必对这些类进行区别,如下图所示。
Composite模式的关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。在图形系统中的这个类就是Graphic,它声明一些与特定图形对象相关的操作,例如Draw。同时它也声明了所有的组合对象共享的一些操作,例如一些操作用于访问和管理它的子部件。
子类Line、Rectangle和Text(参见前面的类图)定义了一些图元对象,这些类实现Draw,分别用于绘制直线、矩形和正文。由于图元都没有子图形,因此它们都不执行与子类有关的操作。
Picture类定义了一个Graphic对象的聚合。Picture的Draw操作是通过对它的子部件调用Draw实现的,Picture还用这种方法实现了一些与其子部件相关的操作。由于Picture接口与Graphic接口是一致的,因此Picture对象可以递归地组合其他Picture对象。
下图是一个典型的由递归组合的Graphic对象组成的组合对象结构。
适用性
以下情况使用Composite模式:
• 你想表示对象的部分-整体层次结构。
• 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
以下是一个简单的 Composite 模式的 C++ 代码示例:
#include
#include
// Component(组件)类:定义叶子节点和组合节点的共同接口
class Component {
public:
virtual void operation() const = 0; // 共同接口
virtual ~Component() = default; // 虚析构函数
};
// Leaf(叶子节点)类:实现 Component 接口的叶子节点
class Leaf : public Component {
public:
void operation() const override {
std::cout << "Leaf operation" << std::endl;
}
};
// Composite(组合节点)类:实现 Component 接口的组合节点,可以包含其他组件
class Composite : public Component {
private:
std::vector<Component*> children; // 包含的子组件
public:
void add(Component* component) {
children.push_back(component);
}
void operation() const override {
std::cout << "Composite operation" << std::endl;
for (const auto& child : children) {
child->operation(); // 调用子组件的操作
}
}
~Composite() override {
for (auto& child : children) {
delete child; // 释放子组件的内存
}
}
};
int main() {
// 创建叶子节点
Leaf leaf1;
Leaf leaf2;
// 创建组合节点,并添加叶子节点
Composite composite;
composite.add(&leaf1);
composite.add(&leaf2);
// 调用组合节点的操作,实际上会调用包含的叶子节点的操作
composite.operation();
return 0;
}
在这个例子中,Component
类是组件的抽象类,有一个纯虚函数 operation()
用于定义叶子节点和组合节点的共同接口。Leaf
类是叶子节点,实现了 Component
接口。Composite
类是组合节点,可以包含其他组件,实现了 Component
接口。main
函数演示了如何使用 Composite 模式创建一个包含叶子节点和组合节点的树状结构,并调用组合节点的操作。
将对象组合成树形结构以表示“部分 -整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
典型的Composite对象结构如下图所示。
参与者
• Component (Graphic)
— 为组合中的对象声明接口。
— 在适当的情况下,实现所有类共有接口的缺省行为。
— 声明一个接口用于访问和管理Component的子组件。
— (可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
• Leaf (Rectangle, Line, Text等)
— 在组合中表示叶节点对象,叶节点没有子节点。
— 在组合中定义图元对象的行为。
• Composite (Picture)
— 定义有子部件的那些部件的行为。
— 存储子部件。
— 在Component接口中实现与子部件有关的操作。
• Client
— 通过Component类接口操纵组合部件的对象。
协作
• 用户使用Component类接口与组合结构中的对象进行交互。如果接收者是一个叶节点,则直接处理请求。如果接收者是Composite,它通常将请求发送给它的子部件,在转发请求之前与/或之后可能执行一些辅助操作。
要点总结
Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化为“一对一”的关系,使得客户代码可以一致地(复用)处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。
将“客户代码与复杂的对象容器结构”解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口而非对象容器的内部实现结构——发生依赖,从而更能“应对变化”。
Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。
在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为 “同一种算法在多种集合对象上进行操作”提供了可能。
使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“应对变化中的集合对象”提供了一种优雅的方式。
一个聚合对象, 如列表(list), 应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构. 此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个表列上同时进行多个遍历。
迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器(iterator)对象中。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素 ; 即, 它知道哪些元素已经遍历过了。
例如, 一个列表(List)类可能需要一个列表迭代器(List iterator), 它们之间的关系如下图:
在实例化列表迭代器之前,必须提供待遍历的列表。一旦有了该列表迭代器的实例,就可以顺序地访问该列表的各个元素。CurrentItem操作返回列表中的当前元素,First操作初始化迭代器,使当前元素指向列表的第一个元素,Next操作将当前元素指针向前推进一步,指向下一个元素,而IsDone检查是否已越过最后一个元素,也就是完成了这次遍历。
将遍历机制与列表对象分离使我们可以定义不同的迭代器来实现不同的遍历策略,而无需在列表接口中列举它们。例如,过滤列表迭代器(FilteringListIterator)可能只访问那些满足特定过滤约束条件的元素。
注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其他聚合结构。最好能有一种办法使得不需改变客户代码即可改变该聚合类。可以通过将迭代器的概念推广到多态迭代(polymorphic iteration)来达到这个目标。
例如,假定我们还有一个列表的特殊实现,比如说SkipList。SkipList是一种具有类似于平衡树性质的随机数据结构。我们希望我们的代码对List和SkipList对象都适用。
首先,定义一个抽象列表类AbstractList,它提供操作列表的公共接口。类似地,我们也需要一个抽象的迭代器类Iterator,它定义公共的迭代接口。然后我们可以为每个不同的列表实现定义具体的Iterator子类。这样迭代机制就与具体的聚合类无关了。
余下的问题是如何创建迭代器。既然要使这些代码不依赖于具体的列表子类,就不能仅仅简单地实例化一个特定的类,而要让列表对象负责创建相应的迭代器。这需要列表对象提供CreateIterator 这样的操作,客户请求调用该操作以获得一个迭代器对象。
创建迭代器是一个 Factory Method 模式的例子。我们在这里用它来使得一个客户可向一个列表对象请求合适的迭代器。Factory Method 模式产生两个类层次,一个是列表的,一个是迭代器的。CreateIterator “联系” 这两个类层次。(工厂方法定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。 Factory Method使一个类的实例化延迟到其子类。)
适用性
迭代器模式可用来:
• 访问一个聚合对象的内容而无需暴露它的内部表示。
• 支持对聚合对象的多种遍历。
• 为遍历不同的聚合结构提供一个统一的接口 (即, 支持多态迭代)。
代码举例
template<typename T>
class Iterator
{
public:
virtual void first() = 0;
virtual void next() = 0;
virtual bool isDone() const = 0;
virtual T& current() = 0;
};
template<typename T>
class MyCollection{
public:
Iterator<T> GetIterator(){
//...
}
};
template<typename T>
class CollectionIterator : public Iterator<T>{
MyCollection<T> mc;
public:
CollectionIterator(const MyCollection<T> & c): mc(c){ }
void first() override {
}
void next() override {
}
bool isDone() const override{
}
T& current() override{
}
};
void MyAlgorithm()
{
MyCollection<int> mc;
Iterator<int> iter= mc.GetIterator();
for (iter.first(); !iter.isDone(); iter.next()){
cout << iter.current() << endl;
}
}
提供一种方法顺序访问一个聚合对象中各个元素 , 而又不需暴露该对象的内部表示。
参与者
• Iterator(迭代器)
— 迭代器定义访问和遍历元素的接口。
• ConcreteIterator(具体迭代器)
— 具体迭代器实现迭代器接口。
— 对该聚合遍历时跟踪当前位置。
• Aggregate(聚合)
— 聚合定义创建相应迭代器对象的接口。
• ConcreteAggregate(具体聚合)
— 具体聚合实现创建相应迭代器的接口,该操作返回ConcreteIterator的一个适当的实例。
协作
• ConcreteIterator跟踪聚合中的当前对象,并能够计算出待遍历的后继对象。
要点总结
迭代抽象:访问一个聚合对象的内容而无需暴露它的内部表示。
迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
在软件构建过程中,一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显式指定,将必不可少地带来请求发送者与接受者的紧耦合。
如何使请求的发送者不需要指定具体的接受者?让请求的接受者自己在运行时决定来处理请求,从而使两者解耦。
chatGPT给出的例子
职责链模式是一种行为设计模式,它允许你将请求沿着处理者链进行发送。每个处理者都负责对请求的一部分进行处理,然后将请求传递给链上的下一个处理者。下面是一个简单的C++代码例子,演示了职责链模式的基本实现:
#include
#include
// 抽象处理者类
class Handler {
public:
virtual void handleRequest(const std::string& request) = 0;
void setSuccessor(Handler* successor) {
successor_ = successor;
}
protected:
Handler* successor_;
};
// 具体处理者类1
class ConcreteHandler1 : public Handler {
public:
void handleRequest(const std::string& request) override {
if (request == "Handler1") {
std::cout << "ConcreteHandler1 handling the request." << std::endl;
} else if (successor_ != nullptr) {
successor_->handleRequest(request);
}
}
};
// 具体处理者类2
class ConcreteHandler2 : public Handler {
public:
void handleRequest(const std::string& request) override {
if (request == "Handler2") {
std::cout << "ConcreteHandler2 handling the request." << std::endl;
} else if (successor_ != nullptr) {
successor_->handleRequest(request);
}
}
};
// 客户端
int main() {
// 创建处理者
Handler* handler1 = new ConcreteHandler1();
Handler* handler2 = new ConcreteHandler2();
// 设置处理者链
handler1->setSuccessor(handler2);
// 发送请求
handler1->handleRequest("Handler1"); // ConcreteHandler1 handling the request.
handler1->handleRequest("Handler2"); // ConcreteHandler2 handling the request.
handler1->handleRequest("Handler3"); // No handler can handle the request.
// 释放资源
delete handler1;
delete handler2;
return 0;
}
在这个例子中,Handler
是抽象处理者类,ConcreteHandler1
和 ConcreteHandler2
是具体处理者类,它们分别处理不同的请求。每个具体处理者类都可以选择处理请求,或将请求传递给下一个处理者。客户端创建了处理者并设置了处理者链,然后通过调用 handleRequest
方法发送请求。
关系解释:
Handler
是抽象处理者类,定义了处理请求的接口,并包含了一个指向下一个处理者的指针。ConcreteHandler1
和 ConcreteHandler2
是具体处理者类,实现了处理请求的具体逻辑,如果无法处理请求,会将请求传递给下一个处理者。handleRequest
方法发送请求。这种设计模式的好处在于它解耦了发送者和接收者,每个处理者只关心自己能够处理的请求,而不需要知道整个处理链的结构。
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
一个典型的对象结构如下所示
参与者
• Handler
— 定义一个处理请求的接口。
— (可选) 实现后继链。
• ConcreteHandler
— 处理它所负责的请求。
— 可访问它的后继者。
— 如果可处理该请求,就处理之;否则将该请求转发给它的后继者。
• Client— 向链上的具体处理者(ConcreteHandler)对象提交请求。
要点总结
Chain of Responsibility模式的应用场合在于“一个请求可能有这时候请求发送多个接受者,但是最后真正的接受者只有一个”者与接受者的耦合有可能出现“变化脆弱”的症状,职责链的目的就是将二者解耦,从而更好地应对变化。
应用了Chain of Responsibility 模式后,对象的职责分派将更具灵活性。我们可以在运行时动态添加/修改请求的处理职责。
如果请求传递到职责链的末尾仍得不到处理,应该有一个合理的缺省机制。这也是每一个接受对象的责任,而不是发出请求的对象的责任。
“行为变化”模式
在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化。“行为变化”模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合。
典型模式
·Command
·Visitor
在软件构建过程中,“行为请求者”与“行为实现者”通常呈现 一种“紧耦合”。但在某些场合——比如需要对行为进行“记录、 撤销/重(undo/redo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。
在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱不能显式在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。
命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的 Command
类,它定义了一个执行操作的接口。其最简单的形式是一个抽象的 Execute
操作。具体的 Command
子类将接收者作为其一个实例变量,并实现 Execute
操作,指定接收者采取的动作。而接收者有执行该请求所需的具体信息。
用Command
对象可很容易的实现菜单(Menu),每一菜单中的选项都是一个菜单项(MenuItem)类的实例。一个Application
类创建这些菜单和它们的菜单项以及其余的用户界面。该Application
类还跟踪用户已打开的Document
对象。
该应用为每一个菜单项配置一个具体的Command
子类的实例。当用户选择了一个菜单项时,该MenuItem
对象调用它的Command
对象的Execute
方法,而Execute
执行相应操作。MenuItem
对象并不知道它们使用的是Command
的哪一个子类。Command
子类里存放着请求的接收者,而Execute
操作将调用该接收者的一个或多个操作。
例如,PasteCommand
支持从剪贴板向一个文档(Document
)粘贴正文。PasteCommand
的接收者是一个文档对象,该对象是实例化时提供的。Execute
操作将调用该Document
的Paste
操作。
而OpenCommand
的Execute
操作却有所不同:它提示用户输入一个文档名,创建一个相应的文档对象,将其作为接收者的应用对象中,并打开该文档。
有时一个MenuItem
需要执行一系列命令。例如,使一个页面按正常大小居中的MenuItem
可由一个CenterDocumentCommand
对象和一个NormalSizeCommand
对象构建。因为这种需将多条命令串接起来的情况很常见,我们定义一个MacroCommand
类来让一个MenuItem
执行任意数目的命令。MacroCommand
是一个具体的Command
子类,它执行一个命令序列。MacroCommand
没有明确的接收者,而序列中的命令各自定义其接收者。
请注意这些例子中Command
模式是怎样解耦调用操作的对象和具有执行该操作所需信息的那个对象的。这使我们在设计用户界面时拥有很大的灵活性。一个应用如果想让一个菜单与一个按钮代表同一项功能,只需让它们共享相应具体Command
子类的同一个实例即可。我们还可以动态地替换Command
对象,这可用于实现上下文有关的菜单。我们也可以通过将几个命令组成更大的命令的形式来支持命令脚本(command scripting)。所有这些之所以成为可能乃是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将会被如何执行。
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
这句话描述了命令模式的核心思想和应用场景。让我们逐步解释其中的关键概念:
将一个请求封装为一个对象: 在命令模式中,命令被封装为对象,使得请求的发送者和接收者之间解耦。命令对象包含了请求的所有信息,包括调用哪个方法、以及执行方法所需的参数等。
使你可用不同的请求对客户进行参数化: 命令模式允许使用不同的命令对象来参数化客户端。客户端可以根据需要选择不同的命令对象,从而实现不同的请求。
对请求排队或记录请求日志: 由于命令对象是实实在在的对象,你可以将它们存储在队列中,实现对请求的排队。此外,你还可以轻松地记录命令对象,用于实现请求的撤销和重做,或者记录请求日志以便后续分析。
支持可撤销的操作: 由于命令对象封装了具体的操作,你可以轻松地实现命令的撤销和重做。通过保存命令的历史记录,你可以在需要时逐步撤销之前的操作,实现可撤销的操作。
综合起来,命令模式允许你将请求以对象的形式进行操作,为你提供了更大的灵活性。这种模式常见于需要支持撤销、日志记录、事务等场景,或者在需要将请求参数化、排队和执行时。
chatGPT给出的例子
现实中的例子:遥控器控制家电
考虑一个遥控器控制家电的情景。你有一台电视、一台音响和一盏灯。现在希望通过遥控器发送命令来控制这些设备的开关和音量。这就是一个典型的命令模式应用。
class Device {
public:
virtual void turnOn() = 0;
virtual void turnOff() = 0;
};
class TV : public Device {
public:
void turnOn() override {
std::cout << "TV is ON" << std::endl;
}
void turnOff() override {
std::cout << "TV is OFF" << std::endl;
}
};
class Stereo : public Device {
public:
void turnOn() override {
std::cout << "Stereo is ON" << std::endl;
}
void turnOff() override {
std::cout << "Stereo is OFF" << std::endl;
}
};
class Command {
public:
virtual void execute() = 0;
};
class TurnOnCommand : public Command {
private:
Device* device;
public:
TurnOnCommand(Device* dev) : device(dev) {}
void execute() override {
device->turnOn();
}
};
class TurnOffCommand : public Command {
private:
Device* device;
public:
TurnOffCommand(Device* dev) : device(dev) {}
void execute() override {
device->turnOff();
}
};
class RemoteControl {
private:
Command* onCommand;
Command* offCommand;
public:
RemoteControl(Command* on, Command* off) : onCommand(on), offCommand(off) {}
void pressOnButton() {
onCommand->execute();
}
void pressOffButton() {
offCommand->execute();
}
};
int main() {
TV tv;
Stereo stereo; // 音响系统
TurnOnCommand turnOnTV(&tv);
TurnOffCommand turnOffTV(&tv);
TurnOnCommand turnOnStereo(&stereo);
TurnOffCommand turnOffStereo(&stereo);
RemoteControl remoteForTV(&turnOnTV, &turnOffTV);
RemoteControl remoteForStereo(&turnOnStereo, &turnOffStereo);
remoteForTV.pressOnButton();
remoteForTV.pressOffButton();
remoteForStereo.pressOnButton();
remoteForStereo.pressOffButton();
return 0;
}
在这个例子中,家电设备(TV、Stereo)实现了通用的 Device
接口,命令对象(TurnOnCommand
、TurnOffCommand
)实现了 Command
接口,而遥控器 RemoteControl
将命令和设备解耦,实现了通过遥控器按钮来控制家电设备的功能。这符合命令模式的设计理念。
上述代码是一个简单的命令模式示例,其中包含以下角色:
Command(命令):
Command
类,是一个抽象类,定义了执行命令的接口。有两个具体的子类:LightOnCommand
和 StereoOnWithCDCommand
。ConcreteCommand(具体命令):
LightOnCommand
和 StereoOnWithCDCommand
,实现了 Command
接口,负责执行具体的操作。在这里,LightOnCommand
负责打开灯,StereoOnWithCDCommand
负责打开音响并播放 CD。Invoker(调用者):
RemoteControl
,负责调用命令对象执行请求。RemoteControl
包含一个按钮,按下按钮会触发相应的命令。Receiver(接收者):
Light
和 Stereo
类,是真正执行命令的对象。Light
负责处理灯的开关,Stereo
负责处理音响的开关和 CD 播放。这个例子模拟了一个遥控器控制各种家用电器的场景。通过命令模式,遥控器不直接处理家用电器的开关和操作,而是通过命令对象,使得可以轻松添加新的命令和控制不同的电器,同时也支持撤销操作。
例如,LightOnCommand
对象表示打开灯的命令,当按钮按下时,RemoteControl
就会调用 execute
方法执行这个命令,具体的执行过程交由 Light
对象处理。这样,通过命令对象,我们实现了命令的封装和解耦,使得调用者和接收者之间的关系更加灵活。
参与者
• Command
— 声明执行操作的接口。
• ConcreteCommand (PasteCommand, OpenCommand)
— 将一个接收者对象绑定于一个动作。
— 调用接收者相应的操作,以实现Execute。
• Client (Application)
— 创建一个具体命令对象并设定它的接收者。
• Invoker (MenuItem)
— 要求该命令执行这个请求。
• Receiver (Document, Application)
— 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
Command模式的优点:
解耦性: Command模式将请求发送者和接收者解耦,请求发送者不需要知道接收者的具体实现,只需要知道如何发送请求即可。这提高了系统的灵活性和可维护性。
容易扩展: 可以轻松地添加新的命令类,无需修改现有的代码。这使得系统在需要添加新功能时更容易扩展。
支持撤销和恢复: Command模式可以实现命令的撤销和重做,通过保存命令的历史记录,可以在需要时进行恢复。
组合命令: 可以将多个命令组合成一个复合命令,从而实现更复杂的操作。
适用于队列和日志: 命令模式常用于构建命令队列,支持将命令保存到日志中,以便后续分析和重放。
总体而言,Command模式在需要解耦请求发送者和接收者、支持撤销和恢复、构建命令队列等情况下是非常有用的,但在简单的场景中可能显得过于繁琐。合适的使用场景可以最大程度地发挥其优势。
在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。
如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?
考虑一个编译器,它将源程序表示为一个抽象语法树。该编译器需在抽象语法树上实施某些操作以进行“静态语义”分析,例如检查是否所有的变量都已经被定义了。它也需要生成代码。因此它可能要定义许多操作以进行类型检查、代码优化、流程分析,检查变量是否在使用前被赋初值,等等。此外,还可使用抽象语法树进行优美格式打印、程序重构、code instrumentation以及对程序进行多种度量。
这些操作大多要求对不同的节点进行不同的处理。例如对代表赋值语句的节点的处理就不同于对代表变量或算术表达式的节点的处理。因此有用于赋值语句的类,有用于变量访问的类,还有用于算术表达式的类,等等。节点类的集合当然依赖于被编译的语言,但对于一个给定的语言其变化不大。
上面的框图显示了Node类层次的一部分。这里的问题是,将所有这些操作分散到各种结点类中会导致整个系统难以理解、难以维护和修改。将类型检查代码与优美格式打印代码或流程分析代码放在一起,将产生混乱。此外,增加新的操作通常需要重新编译所有这些类。如果可以独立地增加新的操作,并且使这些结点类独立于作用于其上的操作,将会更好一些。
要实现上述两个目标,我们可以将每一个类中相关的操作包装在一个独立的对象(称为一个Visitor)中,并在遍历抽象语法树时将此对象传递给当前访问的元素。当一个元素“接受”该访问者时,该元素向访问者发送一个包含自身类信息的请求。该请求同时也将该元素本身作为一个参数。然后访问者将为该元素执行该操作—这一操作以前是在该元素的类中的。
例如,一个不使用访问者的编译器可能会通过在它的抽象语法树上调用TypeCheck操作对一个过程进行类型检查。每一个结点将对调用它的成员的TypeCheck以实现自身的TypeCheck(参见前面的类框图)。如果该编译器使用访问者对一个过程进行类型检查,那么它将会创建一个TypeCheckingVisitor对象,并以这个对象为一个参数在抽象语法树上调用Accept操作。每一个结点在实现Accept时将会回调访问者:一个赋值结点调用访问者的VisitAssignment操作,而一个变量引用将调用VisitVariableReference。以前类AssignmentNode的TypeCheck操作现在成为TypeCheckingVisitor的VisitAssignment操作。
为使访问者不仅仅只做类型检查,我们需要所有抽象语法树的访问者有一个抽象的父类NodeVisitor。NodeVisitor必须为每一个结点类定义一个操作。一个需要计算程序度量的应用将定义NodeVisitor的新的子类,并且将不再需要在结点类中增加与特定应用相关的代码。Visitor模式将每一个编译步骤的操作封装在一个与该步骤相关的Visitor中(参见下图)。
使用Visitor模式,确实需要定义两个类层次:一个是对应于接受操作的元素(Node层次),另一个是对应于定义对元素的操作的访问者(NodeVisitor层次)。给访问者类层次增加一个新的子类即可创建一个新的操作。只要编译器接受的语法不改变(即不需要增加新的Node子类),就可以简单地定义新的NodeVisitor子类以增加新的功能。
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
参与者
• Visitor(访问者,如NodeVisitor)
— 为该对象结构中ConcreteElement的每一个类声明一个Visit操作。该操作的名字和特征标识了发送Visit请求给该访问者的那个类。这使得访问者可以确定正被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
• ConcreteVisitor(具体访问者,如TypeCheckingVisitor)
— 实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片断乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果。
• Element(元素,如Node)
— 定义一个Accept操作,它以一个访问者为参数。
• ConcreteElement(具体元素,如AssignmentNode,VariableRefNode)
— 实现Accept操作,该操作以一个访问者为参数。
• ObjectStructure(对象结构,如Program)
— 能枚举它的元素。
— 可以提供一个高层的接口以允许该访问者访问它的元素。
— 可以是一个复合(参见Composite)或是一个集合,如一个列表或一个无序集合。
让我们考虑一个图形编辑器的情景,其中有不同类型的图形对象(如圆形、矩形、三角形等),每个图形都可以进行不同的操作(如移动、缩放、旋转等)。这是一个典型的使用 Visitor 模式的例子。
首先,定义图形对象的抽象基类:
class Shape;
// Visitor 抽象基类
class Visitor {
public:
virtual void visitCircle(Circle* circle) = 0;
virtual void visitRectangle(Rectangle* rectangle) = 0;
virtual void visitTriangle(Triangle* triangle) = 0;
};
// Shape 抽象基类
class Shape {
public:
virtual void accept(Visitor* visitor) = 0;
};
// 具体图形类 - Circle
class Circle : public Shape {
public:
void accept(Visitor* visitor) override {
visitor->visitCircle(this);
}
// 具体圆形的其他成员和方法
};
// 具体图形类 - Rectangle
class Rectangle : public Shape {
public:
void accept(Visitor* visitor) override {
visitor->visitRectangle(this);
}
// 具体矩形的其他成员和方法
};
// 具体图形类 - Triangle
class Triangle : public Shape {
public:
void accept(Visitor* visitor) override {
visitor->visitTriangle(this);
}
// 具体三角形的其他成员和方法
};
接下来,定义 Visitor 接口和具体的 Visitor 实现:
// 具体 Visitor 实现 - MoveVisitor
class MoveVisitor : public Visitor {
public:
void visitCircle(Circle* circle) override {
// 实现移动圆形的操作
}
void visitRectangle(Rectangle* rectangle) override {
// 实现移动矩形的操作
}
void visitTriangle(Triangle* triangle) override {
// 实现移动三角形的操作
}
};
现在,我们可以使用 Visitor 模式来进行不同操作的访问:
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
MoveVisitor moveVisitor;
circle.accept(&moveVisitor); // 移动圆形
rectangle.accept(&moveVisitor); // 移动矩形
triangle.accept(&moveVisitor); // 移动三角形
return 0;
}
Visitor 模式的原理在于通过在每个具体的图形类中定义 accept
方法,接受一个 Visitor 对象的访问。Visitor 抽象基类中定义了对每种具体图形的访问接口,而具体的 Visitor 实现中实现了对每种图形的具体操作。这样,当我们需要对图形进行不同的操作时,只需创建新的 Visitor 实现,而不需要修改具体图形类。这符合开闭原则,使得系统更加灵活和可扩展。
再添加一个椭圆形时,我们需要修改以下几个地方:
Ellipse
。Visitor
抽象基类,添加访问椭圆形的接口。Visitor
实现,实现对椭圆形的具体操作。main
函数,访问椭圆形。下面是相应的修改:
class Ellipse : public Shape {
public:
void accept(Visitor* visitor) override {
visitor->visitEllipse(this);
}
// 具体椭圆形的其他成员和方法
};
class Visitor {
public:
virtual void visitCircle(Circle* circle) = 0;
virtual void visitRectangle(Rectangle* rectangle) = 0;
virtual void visitTriangle(Triangle* triangle) = 0;
virtual void visitEllipse(Ellipse* ellipse) = 0;
};
class MoveVisitor : public Visitor {
public:
void visitCircle(Circle* circle) override {
// 实现移动圆形的操作
}
void visitRectangle(Rectangle* rectangle) override {
// 实现移动矩形的操作
}
void visitTriangle(Triangle* triangle) override {
// 实现移动三角形的操作
}
void visitEllipse(Ellipse* ellipse) override {
// 实现移动椭圆形的操作
}
};
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
Ellipse ellipse;
MoveVisitor moveVisitor;
circle.accept(&moveVisitor); // 移动圆形
rectangle.accept(&moveVisitor); // 移动矩形
triangle.accept(&moveVisitor); // 移动三角形
ellipse.accept(&moveVisitor); // 移动椭圆形
return 0;
}
通过这样的修改,我们可以灵活地添加新的图形类型,而不影响现有的代码结构。这符合开闭原则,使得系统更加容易扩展。
accept
函数是 Visitor 模式中的一个重要方法,它的作用是在元素对象中注入一个访问者对象,并调用访问者对象相应类型的方法。
在 Visitor 模式中,元素对象(比如图形类)需要与访问者对象进行交互,而元素对象通常并不知道具体要执行哪些操作。为了实现这种松耦合的交互,我们引入了 accept
方法。
以下是 accept
方法的主要作用:
接收访问者对象: accept
方法的参数是一个访问者对象,该对象将执行具体的操作。通过这个方法,元素对象可以接收访问者对象。
调用访问者方法: accept
方法内部调用访问者对象相应类型的方法,将自身传递给访问者。这样,访问者就可以在不改变元素对象的情况下执行所需的操作。
多态分发: accept
方法的调用利用了多态的机制,确保了在运行时调用的是实际类型的访问者方法。这样,我们可以根据不同的元素类型执行不同的操作。
在上面的例子中,accept
方法在 Shape
类中定义,并在具体的图形类(Circle
、Rectangle
、Triangle
、Ellipse
)中进行实现。这样,每个图形对象都可以接收一个访问者,并让访问者执行相应类型的操作。
通过 accept
方法,Visitor 模式实现了元素对象和操作的解耦,同时保留了对不同元素类型的访问能力。这使得我们可以轻松地添加新的访问者和元素类型,而不需要修改已有的代码。
Visitor模式的适用情况:
“领域规则”模式
在特定领域中,某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出在该领域下的一般性解决方案。
典型模式·Interpreter
在软件构建过程中,如果某一特定领域的问题比较复杂,类似的 结构不断重复出现,如果使用普通的编程方式来实现将面临非常频繁的变化。
在这种情况下,将特定领域的问题表达为某种语法规则下的句子, 然后构建一个解释器来解释这样的句子,从而达到解决问题的目的。
代码例子
解释器模式通常用于实现一种特定语言的解释器,它可以用于处理特定领域中的语法或规则。一个典型的例子是正则表达式解释器。正则表达式是一种用于匹配字符串的模式,可以用于文本搜索、替换等操作。
以下是一个简化的正则表达式解释器的 C++ 示例:
#include
#include
#include
// 抽象表达式
class AbstractExpression {
public:
virtual bool interpret(const std::string& context) = 0;
};
// 终结符表达式 - 字符匹配
class TerminalExpression : public AbstractExpression {
private:
std::string pattern;
public:
TerminalExpression(const std::string& pattern) : pattern(pattern) {}
bool interpret(const std::string& context) override {
std::regex regex(pattern);
return std::regex_match(context, regex);
}
};
// 非终结符表达式 - 或操作
class OrExpression : public AbstractExpression {
private:
AbstractExpression* expression1;
AbstractExpression* expression2;
public:
OrExpression(AbstractExpression* expr1, AbstractExpression* expr2)
: expression1(expr1), expression2(expr2) {}
bool interpret(const std::string& context) override {
return expression1->interpret(context) || expression2->interpret(context);
}
};
// 上下文
class Context {
private:
std::string input;
public:
Context(const std::string& input) : input(input) {}
const std::string& getInput() const {
return input;
}
};
int main() {
// 构建解释器规则:匹配 "hello" 或 "world"
AbstractExpression* expression1 = new TerminalExpression("hello");
AbstractExpression* expression2 = new TerminalExpression("world");
AbstractExpression* orExpression = new OrExpression(expression1, expression2);
// 使用解释器进行解释
Context context1("hello");
Context context2("foo");
std::cout << "Is 'hello' matched? " << orExpression->interpret(context1.getInput()) << std::endl;
std::cout << "Is 'foo' matched? " << orExpression->interpret(context2.getInput()) << std::endl;
// 释放资源
delete expression1;
delete expression2;
delete orExpression;
return 0;
}
解释器模式的原理是将语法规则表示为对象,每个规则对应一个表达式类。这些表达式可以组合成复杂的解释器,通过调用 interpret
方法进行解释。在上述例子中,TerminalExpression
表示终结符规则,OrExpression
表示或操作的规则。解释器通过递归调用这些表达式来解释给定的上下文。在实际应用中,解释器模式可用于构建特定领域的查询语言、配置文件解析等场景。
在上述代码中,解释器规则是匹配 “hello” 或 “world”。然后,使用这个规则进行解释。
输出结果应该是:
Is 'hello' matched? 1
Is 'foo' matched? 0
解释器首先检查 “hello” 是否匹配,结果为真(1)。然后,检查 “foo” 是否匹配,结果为假(0)。因此,输出结果显示了匹配的情况。
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
在解释器模式的类图中,通常包含以下几个模块:
AbstractExpression (抽象表达式): 定义解释器的接口,声明一个 interpret
操作,该操作在具体表达式中将被具体解释器实现。
TerminalExpression (终结符表达式): 实现 AbstractExpression
接口,该类表示语言中的终结符,也就是不再被解释的最小单元。通常一个语言中会有多个终结符表达式。
NonterminalExpression (非终结符表达式): 实现 AbstractExpression
接口,该类表示语言中的非终结符,也就是需要进一步解释的表达式。非终结符表达式通常由终结符表达式和其他非终结符表达式组合而成。
Context (上下文): 包含解释器之外的一些全局信息,可能对解释器的解释过程有影响。上下文维护了解释器解释时所需的信息。
Client (客户端): 构建并配置具体的解释器对象,然后将表达式交给解释器进行解释。客户端通常是使用解释器模式的地方。
这些模块之间的关系如下:
AbstractExpression
中声明了 interpret
操作,TerminalExpression
和 NonterminalExpression
分别实现了这个操作。NonterminalExpression
可以包含其他 AbstractExpression
对象,形成解释器的复杂结构。Context
中存储了解释器解释时可能需要用到的信息。Client
负责构建具体的解释器对象,配置解释器,然后调用解释器进行解释。这样的设计使得新增新的表达式或修改解释器的行为变得相对灵活,同时遵循了开闭原则。
解释器模式适用于以下场景:
语法规则频繁变化: 当语法规则经常变化,而系统中的表达式类比较固定时,可以使用解释器模式来灵活地处理变化的规则。
复杂的文法规则: 当需要处理的语法规则非常复杂,难以用简单的语法树表示时,可以考虑使用解释器模式,将文法规则拆分为多个表达式类,每个类负责一部分规则。
执行顺序不同的语法: 如果有需要按照不同的执行顺序解释语法规则,可以使用解释器模式。不同的非终结符表达式类可以通过组合方式实现不同的执行顺序。
优点包括:
灵活性: 可以灵活地改变和扩展文法规则,通过新增新的表达式类来支持新的语法规则。
易于扩展: 新增文法规则或表达式类相对容易,不影响现有的解释器类。
简化规则表达: 使用解释器模式可以将复杂的规则表达式简化为易于理解的表达式类。
分离规则解析和执行: 解释器模式将规则解析和执行分离,使得每个表达式类只关注自己的解释和执行逻辑,提高了代码的可维护性。
总体而言,解释器模式适用于需要处理复杂语法规则,且这些规则经常变化的情景。
截至2024年1月18日20点48分,花费4天时间完成《设计模式》的学习,自己的感受:设计代码的能力有所提升,也可以更好地思考开源库中所应用的设计模式。
结符表达式):** 实现 AbstractExpression
接口,该类表示语言中的非终结符,也就是需要进一步解释的表达式。非终结符表达式通常由终结符表达式和其他非终结符表达式组合而成。
Context (上下文): 包含解释器之外的一些全局信息,可能对解释器的解释过程有影响。上下文维护了解释器解释时所需的信息。
Client (客户端): 构建并配置具体的解释器对象,然后将表达式交给解释器进行解释。客户端通常是使用解释器模式的地方。
这些模块之间的关系如下:
AbstractExpression
中声明了 interpret
操作,TerminalExpression
和 NonterminalExpression
分别实现了这个操作。NonterminalExpression
可以包含其他 AbstractExpression
对象,形成解释器的复杂结构。Context
中存储了解释器解释时可能需要用到的信息。Client
负责构建具体的解释器对象,配置解释器,然后调用解释器进行解释。这样的设计使得新增新的表达式或修改解释器的行为变得相对灵活,同时遵循了开闭原则。
解释器模式适用于以下场景:
语法规则频繁变化: 当语法规则经常变化,而系统中的表达式类比较固定时,可以使用解释器模式来灵活地处理变化的规则。
复杂的文法规则: 当需要处理的语法规则非常复杂,难以用简单的语法树表示时,可以考虑使用解释器模式,将文法规则拆分为多个表达式类,每个类负责一部分规则。
执行顺序不同的语法: 如果有需要按照不同的执行顺序解释语法规则,可以使用解释器模式。不同的非终结符表达式类可以通过组合方式实现不同的执行顺序。
优点包括:
灵活性: 可以灵活地改变和扩展文法规则,通过新增新的表达式类来支持新的语法规则。
易于扩展: 新增文法规则或表达式类相对容易,不影响现有的解释器类。
简化规则表达: 使用解释器模式可以将复杂的规则表达式简化为易于理解的表达式类。
分离规则解析和执行: 解释器模式将规则解析和执行分离,使得每个表达式类只关注自己的解释和执行逻辑,提高了代码的可维护性。
总体而言,解释器模式适用于需要处理复杂语法规则,且这些规则经常变化的情景。
截至2024年1月18日20点48分,花费4天时间完成《设计模式》的学习,自己的感受:设计代码的能力有所提升,也可以更好地思考开源库中所应用的设计模式。包括本文在内的系列笔记所用的图示大部分来自《设计模式–可复用面向对象软件的基础》一书。