C++ 设计模式入门

更新中🔔

设计模式简介

C++设计模式入门

设计模式:可复用面向对象软件的基础,可复用是设计模式的目的。

深入理解面向对象

  1. 向下:三大特性
    1. 封装:隐藏内部实现
    2. 继承:复用现有代码
    3. 多态:改写对象行为
  2. 向上:抽象的设计意义,使用三大特性来表达世界

设计模式的 6 大设计原则

  1. 单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。
  2. 开放封闭原则:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。
  3. 里氏代换原则:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中 ,把基类替换成派生类,程序的行为没有变化。
  4. 依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要针对实现编程。
  5. 迪米特原则:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
  6. 接口隔离原则:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。

设计模式分类

  1. 创造型模式:单例模式、工厂模式、建造者模式、原型模式
  2. 结构型模式:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式
  3. 行为型模式:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式

从封装变化的角度对设计模式分类

分类

分类

常见的几种设计模式

  1. 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点
  2. 工厂模式:包括简单工厂模式、抽象工厂模式、工厂方法模式
    1. 简单工厂模式:主要用于创建对象。用一个工厂来根据输入的条件产生不同的类,然后根据不同类的虚函数得到不同的结果。
    2. 工厂方法模式:修正了简单工厂模式中不遵守开放封闭原则。把选择判断移到了客户端去实现,如果想添加新功能就不用修改原来的类,直接修改客户端即可。
    3. 抽象工厂模式:定义了一个创建一系列相关或相互依赖的接口,而无需指定他们的具体类。
  3. 观察者模式:定义了一种一对多的关系,让多个观察对象同时监听一个主题对象,主题对象发生变化时,会通知所有的观察者,使他们能够更新自己。
  4. 装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成派生类更为灵活。

组件协作

观察者模式

动机

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

模式定义

定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《 设计模式》 GoF

结构

观察者模式

例子✨✨

文件分割器:将一个大文件分割为几个小文件,添加一个进度条。

  1. 在 FileSplitter 类中添加具体的通知控件进度条类 ProgressBar
    1. 违背了依赖倒置原则,进度条的更新依赖 ProgressBar 的内部实现
    2. 进度通知的展现方式可能不同:GUI界面、控制台等
  2. 观察者模式
    1. 添加抽象的通知机制 IProgress(接口),不耦合界面
    2. 观察者类继承 IProgress 接口,去定义具体的进度条更新 DoProgress(观察者和进度条是紧耦合的)
    3. 如果有多个观察者,则将添加观察者列表,添加 add 和 remove 方法

要点总结

  • 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
  • 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
  • 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
  • Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。

单一职责

装饰模式

动机(Motivation)

  • 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性; 并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
  • 如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?

模式定义

动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代 码 & 减少子类个数)。 ——《设计模式》GoF

结构

例子✨✨

IO流(文件流、网络流、内存流)的业务操作(Read、Seek、Write),添加额外的加密、缓冲操作

  1. 继承

继承

​ 问题:类的规模急剧增加,但加密、缓冲操作相同,不同的只有流的业务操作,代码大量重复

  1. 装饰模式

装饰

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//业务操作
class Stream{
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;

virtual ~Stream(){}
};

//主体类
class FileStream: public Stream{
public:
virtual char Read(int number){
//读文件流
}
virtual void Seek(int position){
//定位文件流
}
virtual void Write(char data){
//写文件流
}

};

class NetworkStream :public Stream{
public:
virtual char Read(int number){
//读网络流
}
virtual void Seek(int position){
//定位网络流
}
virtual void Write(char data){
//写网络流
}

};

class MemoryStream :public Stream{
public:
virtual char Read(int number){
//读内存流
}
virtual void Seek(int position){
//定位内存流
}
virtual void Write(char data){
//写内存流
}
};

//扩展操作
// 由于两个子类有相同的成员Stream*,所以这个成员要往上提
DecoratorStream: public Stream{
protected:
Stream* stream;//...
DecoratorStream(Stream * stm):stream(stm){
}
};

class CryptoStream: public DecoratorStream {
public:
CryptoStream(Stream* stm):DecoratorStream(stm){
}
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
stream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream::Write(data);//写文件流
//额外的加密操作...
}
};

class BufferedStream : public DecoratorStream{
Stream* stream;//...

public:
BufferedStream(Stream* stm):DecoratorStream(stm){

}
//...
};

void Process(){

//运行时装配
FileStream* s1=new FileStream();
CryptoStream* s2=new CryptoStream(s1);
BufferedStream* s3=new BufferedStream(s1);
BufferedStream* s4=new BufferedStream(s2);
}

要点总结

  • 通过采用组合而非继承的手法, Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。 避免了使用继承带来的“灵活性差”和“多子类衍生问题”。
  • Decorator类在接口上表现为is-a Component的继承关系,即Decorator类继承了Component类所具有的接口。 但在实现上又表现为has-a Component的组合关系,即Decorator类又使用了另外一个Component类。
  • Decorator模式的目的并非解决“多子类衍生的多继承”问题,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——是为“装饰”的含义。

对象创建

工厂方法模式

动机

  • 在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
  • 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合?

模式定义

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(目的:解耦,手段:虚函数)到子类。 ——《设计模式》GoF

结构

工厂方法模式

例子✨✨

文件分割器,未来变化的需求:多种文件(二进制文件、文本文件、图片文件、视频文件)

  1. 定义抽象分割器类 ISplitter ,多种文件分割器继承这个抽象分割器类

    违背依赖倒置原则,主界面 MainForm 类依赖于具体分割器类的构造函数(细节)

  2. 工厂方法模式

    定义SplitterFactory分割器工厂基类,声明CreateSplitter接口,具体的分割器工厂继承分割器工厂基类类,实现具体CreateSplitter,MainForm构造时使用一个基类的指针接受具体的分割器工厂指针,调用分割器工厂类的CreateSplitter方法创建具体的分割器对象

要点总结

  • Factory Method模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
  • Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
  • Factory Method模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同。

抽象工厂模式

动机

  • 在软件系统中,经常面临着“一系列相互依赖的对象工作”;同时,由于需求的变化,往往存在更多系列对象的创建工作。
  • 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“多系列具体对象创建工作”的紧耦合。

模式定义

提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。 ——《设计模式》GoF

结构

抽象工厂

例子✨✨

数据库一系列操作 DBConnection、DBCommand、DBReader,多种数据库SQL、Oracle等

  1. 工厂方法模式

    3个具有相关性的工厂

  2. 抽象工厂模式

    将3个工厂合并为一个工厂,内部提供创建一系列的相关对象的接口

要点总结

  • 如果没有应对”多系列对象创建“的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的工厂即可。
  • ”系列对象“指的是在某一个特定系列的对象之间有相互依赖、或作用的关系。不同系列的对象之间不能相互依赖。
  • Abstract Factory模式主要在于应用”新系列“的需求变动。其缺点在与难以应对”新对象“的需求变动。

对象性能

单例模式

动机(Motivation)

  • 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。
  • 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
  • 这应该是类设计者的责任,而不是使用者的责任。

模式定义

保证一个类仅有一个实例,并提供一个该实例的全局访问点。 ——《设计模式》GoF

结构

单例模式

实现✨✨

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};

Singleton* Singleton::m_instance=nullptr;

//线程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}

//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}

//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {

if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}

//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}

要点总结

  • Singleton模式中的实例构造器可以设置为protected以允许子类派生。
  • Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初中违背。
  • 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现(编译器内存读写reorder不安全,volatile,std::atomic标准库)。

数据结构

职责链模式

动机(Motivation)

  • 一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接收者,如果显式指定,将必不可少地带来请求发送者与接收者的紧耦合。
  • 如何使请求的发送者不需要指定具体的接收者?让请求的接收者自己在运行时决定来处理请求,从而使两者解耦。

模式定义

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

结构

职责链模式

要点总结

  • 应用于”一个请求可能有多个接受者,但是最后真正的接受者只有一个“,这时候请求发送者与接受者有可能出现”变化脆弱“的症状,职责链解耦。
  • 有些过时。

C++ 设计模式入门
https://ww1820.github.io/posts/32bc7edb/
作者
AWei
发布于
2022年10月25日
更新于
2022年10月25日
许可协议