常用设计模式

简介

三类模式

  • 创建型模式(Creational Patterns):隐藏对象创建逻辑
  • 结构型模式(Structural Patterns):类和对象的组合
  • 行为型模式(Behavioral Patterns):对象之间的通信

模式之间的关系图:

六大原则

  • 开闭原则(Open Close Principle):对扩展开放,对修改关闭
  • 里氏代换原则(Liskov Substitution Principle):子类可以替换掉父类
  • 依赖倒转原则(Dependence Inversion Principle):针对接口编程
  • 接口隔离原则(Interface Segregation Principle):使用多个隔离的接口比使用单个接口好
  • 迪米特法则(Demeter Principle):实体之间尽量少发生相互作用,模块独立。又称为最少知道原则
  • 合成复用原则(Composite Reuse Principle):尽量用组合,而不是继承

不应该过分纠结特定语言下的实现,而是应该认真体会其设计思想,也就是六大原则的运用

创建型

工厂模式(Factory Pattern)

一个产品基类、多个具体产品;一个工厂基类、多个具体工厂。

具体工厂和具体产品一一对应

使用工厂可以避免依赖于具体的产品类,方便扩展,而且还可以实现对象池。

抽象工厂模式(Abstract Factory Pattern)

多个抽象产品,工厂基类中也有多个创建接口。

一个具体工厂创建一个系列的具体产品(比如A1和B1来自同一个工厂,A2和B2来自另一个工厂)

单例模式(Singleton Pattern)

一个类只有一个实例。隐藏它的创建方法,外界只需要知道如何访问到唯一的实例。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
public:
// 禁止外部创建
Singleton(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator= (const Singleton&) = delete;
Singleton& operator= (Singleton&&) = delete;

static auto& GetInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};

生成器模式(Builder Pattern)

当一个类型的构造函数参数很多,或者构造过程繁琐的时候,把构造过程封装到对象里,也就是生成器。由生成器提供各种build方法,由指挥者按顺序执行。

因为太过繁杂,用重载构造函数可能更简单。

使用

1
2
3
4
5
Director director;

CarBuilder builder;
director.constructSportCar(builder); // 传递特定的builder
Car car = builder.getProduct(); // 复杂的设置参数过程就被隐藏了

原型模式(Prototype Pattern)

用一个哈希表保存子类对象的实例或者创建函数(统一接口),从而实现由基类创建子类对象。

可以参考这篇文章的应用

结构型

适配器模式(Adapter Pattern)

原本的接口不兼容,那么就使用一个适配器进行封装,对外提供需要的接口。实际上是在内部进行了转换之后再使用原有的接口

这个封装的过程可以用组合,也可以用继承。可以认为是把转换的过程封装到了对象里。

STL里的stackqueue的默认实现就是对deque的接口进行了简单的封装,可以认为也是一种适配器。

桥接模式(Bridge Pattern)

把具体的实现交给内部组合的对象,虽然对外接口不变,但是表现的行为已经可以根据组合对象而变。

  • 可以将类的多种功能分派给其它类,方便修改

  • 通过派生一个类,只能获得一个维度上的扩展。如果使用对象组合,就可以获得二维的扩展

  • 可以很方便地改变底层,从而实现跨平台

桥接模式的一种特例是「Pimpl Idiom」,不仅把具体实现完全封装在内部对象中,而且只保存了它的指针,在隐藏了实现细节的同时还减少了头文件的大小。(如果要使用智能指针,需要注意析构函数需要出现在实现类的完整定义之后)

实现Pimpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Test.h
class Test {
public:
Test() = default;
~Test();
private:
class impl;
std::unique_ptr<impl> _impl; // 注意unique_ptr不能拷贝,需要自行实现拷贝函数
};

// Test.cpp
class Test::impl{ // 也可以放到private头文件中,然后include到这里
// 实现细节
};

Test::Test() : _impl(std::make_unique<Test::impl>()) {}
Test::~Test() = default;

组合模式(Comopsite Pattern)

前面已经用过很多次了,就是在对象中组合其它对象,称为组件

装饰器模式(Decorator Pattern)

也是组合模式的应用,同时还在覆写基类方法时调用了组合对象的方法,关键就在于这个组合的对象可能也组合了其它对象,于是最终这个方法被调用后的结果将是层层包装过的,这也就是装饰。

为了实现这样任意的组合,需要具体的组件和装饰器都实现了组件接口,并且在装饰器里组合了一个组件

具体实现可见这篇文章

门面模式(Facade Pattern)

封装复杂的子系统,暴露给外界一个简单的接口。

享元模式(Flyweight Pattern)

将具有重复不可变数据的对象缓存起来,使用的时候应用不同的外在状态即可,而不用重复创建对应状态的对象。比如在屏幕上绘制多个相同的图片,实际上图片资源只有一份,只不过绘制的位置的不一样。

代理模式(Proxy Pattern)

还是先进行对象组合,然后对外提供与组合对象一致的接口,只是内部可能已经进行缓存之类的特殊处理。

行为型

责任链模式(Chain of Responsibility Pattern)

假设某件事情需要经过多个步骤进行处理,这些步骤还可能变化,这时可以把整个流程看成一个链表,每个步骤就是一个链表节点,从而实现动态的处理流程

命令模式(Command Pattern)

类似于责任链模式,将每个动作封装为对象。这里的动作称为命令,统一有个执行命令的接口。需要调用操作的地方只需要创建对应的命令实例,从而实现代码复用。

可以在运行时改变命令;可以实现操作回滚

迭代器模式(Iterator Pattern)

把容器内部实现隐藏起来,对外提供一个遍历访问容器元素的接口(迭代器本身是个对象,应该说是迭代器向外部提供了接口)

这在STL里很常见,STL容器对应的迭代器统一使用了using别名,比如list::iterator,很容易获取

应用

1
2
3
4
5
6
7
8
9
std::list l{ 9, 0, 1 ,3 };
for (std::list<int>::iterator itor{ l.begin() }; itor != l.end(); itor++) {
// get *itor to do something
}

// C++11有了auto和基于范围的for之后就不需要写得这么麻烦了,但是底层实际上还是在调用迭代器
for(auto item : l){
// ...
}

中介者模式(Mediator Pattern)

多个对象之间并不直接通信,而是添加一个中间类,所有需要交互的对象都访问中间类。

就好比是聊天室:所有人都只需要把消息发送给消息中心而不用知道其它人的存在,消息中心保存了所有参与者的引用并负责群发消息

备忘录模式(Memento Pattern)

对象的状态需要保存,但是又不能把所有数据都暴露出去,于是可以采用一个嵌套类来记录自身状态。这样被保存的类可以访问嵌套类的所有数据,而外界状态栈只需要保存这些状态对象又不会访问到里面的具体数据

观察者模式(Observer Pattern)

可以实现一对多的依赖关系:发布者保存了订阅者的列表,当状态改变时,就遍历订阅者进行通知

一种实现可以参考这篇文章:C++不定长参数委托

状态模式(State Pattern)

最直接的应用就是有限状态机,把对象所有状态都抽象成一个一个类,每个状态类都实现了当前状态下的执行接口,同时上下文本身还要支持状态间的转换

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StateBase {
public:
virtual void Update() = 0;
};

class Context {
public:
void Update() {
_currentState->Update();
}
void ChangeTo(StateBase* newState) {
_currentState = newState;
}
private:
StateBase* _currentState;
};

class State1 : public StateBase{
public:
virtual void Update() override {
// do something
}
};

策略模式(Strategy Pattern)

把算法封装到对象中,最直接的应用就是STL里的仿函数,也就是一个个函数对象

与状态模式的区别在于:策略之间不需要相互转换,也就不需要知道其它策略

应用

1
2
3
4
5
std::vector v{ 9, 0, 1, 3 };
auto cmp = [](const int& a, const int& b) { // 这就是一种策略
return a < b;
};
std::sort(v.begin(), v.end(), cmp);

模板方法模式(Template Method Pattern)

这里的模板指的是方法的执行顺序已经基本确定下来,子类只需要对具体的方法进行重写,而不需要改变其流程

实现

1
2
3
4
5
6
7
8
9
10
11
12
class APP {
public:
void TemplateMethod() { // 固定流程
Step1();
if(Step2()) {
Step3();
}
}
virtual void Step1() = 0; // 其中的具体操作
virtual bool Step2() = 0;
virtual void Step3() = 0;
};

访问者模式(Visitor Pattern)

主要解决的问题是希望添加新功能,但是新添加的功能可能跟原有的功能不相符,也就是不符合单一职责原则,所以不想直接把功能代码加进来。于是需要另外一个称为访问者的类,由它接收对象后完成新功能。

看起来用函数重载就可以完成,但是问题在于我们遍历对象时用的肯定是一个统一的基类,当传入重载函数之后只能进入基类型对应的重载函数,并不能得到它真正的泛化类型。

解决方法是「双分派」:让被访问者实现accept方法,在内部调用访问者的功能函数,这样就一定可以传递为其真实的类型了。详细描述可以看这里

实现

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
36
37
/*
* Visitor.h
*/
class Child1; // 一定要用前向声明而不能直接引入头文件,不然会发生循环引用
class Child2;

class Visitor { // 定义访问接口,针对具体类型的静态绑定
public:
void Visit(const Child1& c1); // .cpp中:std::cout << "Visit Child1\n";
void Visit(const Child2& c2); // .cpp中:std::cout << "Visit Child2\n";
};

/*
* Visitable.h
*/
class Visitor; // 一定要用前向声明

class Visitable { // 定义接收接口,动态绑定
public:
virtual void Accept(Visitor& v) = 0;
};

/*
* Child1.h
*/
class Child1 : public Visitable {
public:
virtual void Accept(Visitor& v) override; // .cpp中:v.Visit(*this);
};

/*
* Child2.h
*/
class Child2 : public Visitable {
public:
virtual void Accept(Visitor& v) override; // .cpp中:v.Visit(*this);
};

原本遍历一个集合中的对象时,可能需要先通过dynamic_cast判断类型再调用接口,现在可以直接调用

使用

1
2
3
4
5
6
7
8
9
Visitable* cs[] = {new Child1(), new Child2()};
Visitor v;
for (auto c : cs) {
c->Accept(v); // 动态绑定到两个子类实现的接口,而子类实现里的调用的Visit是函数重载(静态绑定)
}

// 输出
Visit Child1
Visit Child2

除了增加访问接口外,其余的代码放在访问者中,对原有类的影响比较小

参考

[1] https://www.runoob.com/design-pattern/design-pattern-tutorial.html

[2] https://refactoringguru.cn/design-patterns