0%

C++设计模式速成笔记

学习资源:
设计模式 | 爱编程的大丙
Refactoring.Guru 设计模式

  • C++设计模式速成笔记
    • 预备知识
    • 设计模式三原则
    • 设计模式一览
    • 创建型模式
      • 工厂方法模式 Factory Method
      • 抽象工厂模式 Abstract Factory
      • 单例模式 Singleton
      • 原型模式 Prototype
    • 结构型模式
      • 享元模式 Flyweight
    • 行为模式
      • 命令模式 Command
      • 策略模式 Strategy
      • 状态模式 State
      • 观察者模式 Observer

C++设计模式速成笔记

预备知识

什么是设计模式:三大类 二十七种

创建型模式:这类模式提供创建对象的机制,能够提升已有代码的灵活性和可复用性
结构型模式:这类模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效
行为模式:这类模式负责对象间的高效沟通和职责委派

面向对象语言:封装 继承 多态
大象装冰箱 面对过程 面向对象 对象即类的实例
封装:把一些属性(成员变量)与行为(成员函数)封装到一个类
继承:子类继承父类(公共或受保护的成员变量和成员属性,即非私有的)
多态:同名函数具有不同的状态:
需要发生继承,父类定义虚函数,子类重写父类的虚函数,父类的指针或引用指向子类对象并通过其调用子类从父类继承来的虚函数(概念:纯虚函数,抽象类)

UML类图 unified modeling meeting
通过UML把对应的类的结构描述出来
把类与类之间的关系清晰的表述出来:继承、关联、聚合、组合、依赖

描述类结构

可见性:+ public # protected - private _ static(用加下划线的方式表示static)
属性:【可见性】【属性名称】:【类型】=【缺省值,可选】
- m_gunName:string = “AK-47”
方法:【可见性】【方法名称】(【参数名:参数类型, …】):【返回值类型】
- shoot():void
注:抽象类,类名用斜体,虚函数跟随类名也使用斜体,纯虚函数最后给函数指定 =0

描述类关系

  • 继承(泛化Generalization):带空心三角形的实线
  • 关联Assocition(例如将一个类的对象作为另一个类的成员变量):
    • 单向关联:带单向箭头的实线
    • 双向关联:带双向箭头的实线
    • 自关联(当前类中包含一个自身类型的对象成员,例如链表):箭头指向自己的实线
    • 关联平等,聚合不平等
  • 聚合Aggregation(整体与部分,成员对象是整体的一部分,且可以脱离整体对象独立存在)
    • 代码实现聚合关系,成员对象通常以构造方法、Setter 方法的方式注入到整体对象之中
    • 带空心菱形的实线
  • 组合Composition(整体与部分,整体对象控制成员对象的生命周期,同生共死)
    • 代码实现组合关系,通常在整体类的构造方法中直接实例化成员类,整体对象析构其子对象一并析构
    • 带实心菱形的实线
  • 依赖Dependency(使用):带箭头的虚线
    • 将一个类的对象作为另一个类中方法的参数
    • 在一个类的方法中将另一个类的对象作为其对象的局部变量
    • 在一个类的方法中调用另一个类的静态方法

设计模式三原则

  • 单一职责原则
    • 对一个类而言,应该仅有一个引起它变化的原因,其实就是将这个类所承担的职责单一化(解耦合)
  • 开放封闭原则
    • 软件实体(类、模块、函数等)可以扩展,但是不可以修改,即对于扩展是开放的,对于修改是封闭的,其实就是实现多态,创建新的子类并重写父类虚函数,用以更新处理动作
  • 依赖倒转原则
    • 高层模块不应该依赖低层模块,两个都应该依赖抽象
    • 抽象不应该依赖细节,细节应该依赖抽象

关于依赖倒转原则

高层模块:可以理解为上层应用,就是业务层的实现
低层模块:可以理解为底层接口,比如封装好的 API、动态库等
抽象:指的就是抽象类或者接口,在 C++ 中没有接口,只有抽象类
如果高层模块依赖低层模块,当依赖的低层模块改变,高层代码也要进行对应的修改,无法实现对高层代码的直接复用

里氏代换原则:子类类型必须能够替换掉它们的父类类型
需满足:1. 继承 2. 子类继承的所有父类的属性和方法对于子类来说都是合理的
条件满足后,在实际应用中就可以使用子类替换掉父类,同时功能也不会受到影响,父类实现了复用,子类也能在父类的基础上增加新的行为,这个就是里氏代换原则

抽象不应该依赖细节,细节应该依赖抽象。也就意味着我们应该对细节进行封装,在 C++ 中就是将其放到一个抽象类中(C++ 中没有接口,不能像 Java 一样封装成接口)

  • 抽象类中提供的接口是固定不变的

  • 低层模块是抽象类的子类,继承了抽象类的接口,并且可以重写这些接口的行为

  • 高层模块想要实现某些功能,调用的是抽象类中的函数接口,并且是通过抽象类的父类指针引用其子类的实例对象(用子类类型替换父类类型),这样就实现了多态

设计模式一览

创建型模式

工厂方法模式 Factory Method

工厂方法模式是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型

它的别称 虚拟构造函数(Virtual Constructor)很好的描述了它的实质,即使用特殊的工厂方法代替对于对象构造函数的直接调用,工厂方法返回的对象即为“产品”

简单工厂模式与工厂模式的不同在于其只使用一个工厂类来生产许多不同的产品对象,这种方式违反了设计模式中的开放—封闭原则,在工厂模式中,用一个基类包含一个虚工厂函数用于实现多态,再用多个子类重写父类的工厂函数

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 Product
{
public:
virtual void doStuff() = 0;
virtual ~Product() {}
};

class ConcreteProductA
{
public:
void doStuff() override
{
// do something
}
};

class ConcreteProductB
{
public:
void doStuff() override
{
// do something else
}
};

class Creator
{
public:
virtual Product* createProduct() = 0;
Virtual ~Creator() {}
};

class ConcreteCreatorA : public Creator
{
public:
Product* createProduct() override
{
return new ConcreteProductA();
}
~ConcreteCreatorA() {}
}

class ConcreteCreatorB : public Creator
{
public:
Product* createProduct() override
{
return new ConcreteProductB();
}
~ConcreteCreatorB() {}
}

int main()
{
Creator* factory = new ConcreteCreatorA;
Product* obj = factory->createProduct();
obj->doStuff();
return 0;
}

抽象工厂模式 Abstract Factory

抽象工厂模式是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类

什么是 系列对象? 例如有这样一组的对象: ​ 运输工具外壳 + 运动引擎 + 控制系统,它可能会有几个变体:

汽车钢板外壳 + 内燃机 + 方向盘离合油门刹车(汽车控制系统)
飞机钛合金外壳 + 喷气式发动机 + 操纵杆(飞机控制系统)

另一个例子:使用现代Modern、维多利亚Victorian、装饰风艺术Art­Deco等风格生成不同的椅子、沙发和咖啡桌

抽象工厂模式给一系列功能相同但是属性会发生变化的组件添加一个抽象类,这样就可以非常方便地进行后续的拓展,再搭配工厂类就可以创建出我们需要的对象了

工厂模式创建的对象对应的类不需要提供抽象类,而抽象工厂模式创建的对象对应的类有抽象的基类,其对应的产品类组件中有可变因素

如果抽象工厂模式创建的一系列相关对象需要组装成一个整体,可以定义整体类,然后直接在具体工厂中组装出来

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
// ProductA
class AbstractProductA
{
public:
virtual void fuction() = 0;
virtual ~AbstractProductA() {}
};

class ConcreteProductA1 : public AbstractProductA
{
public:
void fuction() override
{
// ProductA1 do something
}
}

class ConcreteProductA2 : public AbstractProductA
{
public:
void fuction() override
{
// ProductA2 do something
}
}

// ProductB
// as same as ProductA

// AbstractFactory
class AbstractFactory
{
public:
virtual ProductA* createProductA() = 0;
virtual ProductB* createProductB() = 0;
virtual ~AbstractFactory() {}
};

// ConcreteFactory1
// create ProductA1 and ProductB1
class ConcreteFactory1 : public AbstractFactory
{
public:
ProductA* createProductA() override
{
return new ConcreteProductA1();
}
ProductB* createProductB() override
{
return new ConcreteProductB1();
}
}

// ConcreteFactory2
// create ProductA2 and ProductB2
// as same as ConcreteFactory1

class Client
{
public:
Client(AbstractFactory f) : factory(f) {}
void someOption();
private:
AbstractFactory factory;
}

单例模式 Singleton

单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并为该实例提供一个全局访问节点,单例模式同时解决了两个问题,所以违反了单一职责原则

要保证一个类的实例有且仅有一个,就必须采取一些防护措施,涉及一个类多对象操作的函数有以下几个:

  • 构造函数:创建一个新的对象
  • 拷贝构造函数:根据已有对象拷贝出一个新的对象
  • 拷贝赋值操作符重载函数:两个对象之间的赋值

作以下处理:

  • 构造函数私有化,防止其他对象使用单例类的 new 运算符,
    构造函数在类内部只调用一次
    • 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是 静态的,这样就可以通过类名来访问了,为了不破坏类的封装,将这个静态对象的访问权限设置为 私有的
    • 在类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象,该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在这个静态成员变量中,此后所有对于该函数的调用都将返回这一缓存对象
  • 拷贝构造函数私有化或者禁用
  • 拷贝赋值操作符重载函数私有化或者禁用
1
2
3
4
5
6
7
8
9
10
11
12
// 单例模式的类
class Singleton
{
public:
// = delete 代表函数禁用,也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
static Singleton* getInstance();
private:
Singleton() = default;
static Singleton* m_obj;
}

原型模式 Prototype

原型模式是一种创建型设计模式,使你能够复制已有对象,而又无需使代码依赖它们所属的类

原型模式将克隆过程委派给被克隆的实际对象。模式为所有支持克隆的对象声明了一个通用接口,该接口让你能够克隆对象,同时又无需将代码和对象所属类耦合。通常情况下,这样的接口中仅包含一个克隆方法

注:拷贝构造函数的局限:克隆可能会在父类和子类之间进行,并且可能是动态的,很明显通过父类的拷贝构造函数无法实现对子类对象的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Prototype {
public:
Prototype() {}
virtual ~Prototype() {}
virtual Prototype* Clone() const = 0;
};

class ConcretePrototype : public Prototype {
public:
ConcretePrototype() {}
Prototype* Clone() const override {
return new ConcretePrototype(*this);
}
};

结构型模式

享元模式 Flyweight

享元模式是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象

对象的常量数据通常被称为 内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象“从外部”改变,因此被称为 外在状态。例:射击游戏的粒子Particle类中子弹的颜色和精灵图sprite都一样,为其内在状态(其实也是消耗内存最多的成员变量,所以为了节约内存容量,对其进行复用而不是重复存储),但它的另一些状态(坐标、移动矢量和速度)的数值不断变化,为其外在状态

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法,程序只在对象中保存内在状态,以便复用, 这些对象的区别仅在于其内在状态,与外在状态相比,内在状态的变体要少很多,于是对象数量会大大削减,将仅存储内在状态的对象称为 享元,须确保其状态不能被修改(享元类的状态只能由构造函数的参数进行一次性初始化,不能对其他对象公开其设置器或公有成员变量),而外在状态会被移动到容器对象中,也就是我们应用享元模式前的聚合对象中,或者创建独立的情景类来存储外在状态和对享元对象的引用

为了能更方便地访问各种享元,你可以创建一个工厂方法来管理已有享元对象的缓存池。工厂方法从客户端处接收目标享元对象的内在状态作为参数,如果它能在缓存池中找到所需享元,则将其返回给客户端;如果没有找到,它就会新建一个享元,并将其添加到缓存池中

可以选择在程序的不同地方放入该函数。最简单的选择就是将其放置在享元容器中。除此之外,还可以新建一个工厂类,或者创建一个静态的工厂方法并将其放入实际的享元类中

行为模式

命令模式 Command

命令模式是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作

例:开发文字编辑器中包含多个按钮的工具栏,GUI层将工作委派给业务逻辑底层,命令模式建议GUI对象不直接提交这些请求,而是将请求的所有细节抽取出来组成命令类,命令类是减少 GUI 和业务逻辑层之间耦合的中间层

策略模式 Strategy

策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换

名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用,上下文并不执行任务,而是将工作委派给已连接的策略对象

上下文不负责选择符合任务需要的算法——客户端会将所需策略传递给上下文。实际上,上下文并不十分了解策略,它会通过同样的通用接口与所有策略进行交互,而该接口只需暴露一个方法来触发所选策略中封装的算法即可

因此,上下文可独立于具体策略。这样就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了

应用场景:不同交通工具的路线规划

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
class Strategy
{
public:
virtual ~Strategy() = default;
virtual std::string doAlgorithm(std::string_view data) const = 0;
//c++ 17中,标准库新增了一个特殊的字符串类std::string_view
//它允许我们像std::string一样处理字符序列,而不需要为这些字符序列分配内存
};

class Context
{
private:
std::unique_ptr<Strategy> strategy_;
public:
explicit Context(std::unique_ptr<Strategy> &&strategy = {}) : strategy_(std::move(strategy))
{
}
void set_strategy(std::unique_ptr<Strategy> &&strategy)
{
strategy_ = std::move(strategy);
}
void doSomeBusinessLogic() const
{
if (strategy_) {
std::cout << "Context: Sorting data using the strategy (not sure how it'll do it)\n";
std::string result = strategy_->doAlgorithm("aecbd");
std::cout << result << "\n";
} else {
std::cout << "Context: Strategy isn't set\n";
}
}
};

class ConcreteStrategyA : public Strategy
{
public:
std::string doAlgorithm(std::string_view data) const override
{
std::string result(data);
std::sort(std::begin(result), std::end(result));

return result;
}
};
class ConcreteStrategyB : public Strategy
{
std::string doAlgorithm(std::string_view data) const override
{
std::string result(data);
std::sort(std::begin(result), std::end(result), std::greater<>());

return result;
}
};

void clientCode()
{
Context context(std::make_unique<ConcreteStrategyA>());
std::cout << "Client: Strategy is set to normal sorting.\n";
context.doSomeBusinessLogic();
std::cout << "\n";
std::cout << "Client: Strategy is set to reverse sorting.\n";
context.set_strategy(std::make_unique<ConcreteStrategyB>());
context.doSomeBusinessLogic();
}

int main()
{
clientCode();
return 0;
}

/*
Output.txt:执行结果

Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
abcde

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
edcba
*/

状态模式 State

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样

状态模式与 有限状态机 的概念紧密相关,其主要思想是程序在任意时刻仅可处于几种 有限状态 中。在任何一个特定状态中,程序的行为都不相同,且可瞬间从一个状态切换到另一个状态。不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变。这些数量有限且预先定义的状态切换规则被称为 转移

将该方法应用在对象上,假如有一个文档 Document 类,文档可能会处于草稿 Draft、审阅中 Moderation 和已发布 Published 三种状态中的一种。文档的 publish 发布方法在不同状态下的行为略有不同:

  • 处于草稿状态时,它会将文档转移到审阅中状态
  • 处于审阅中状态时,如果当前用户是管理员,它会公开发布文档
  • 处于已发布状态时,它不会进行任何操作

基于条件语句的状态机,代码的维护工作非常艰难,随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻,而状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中

原始对象被称为上下文(context),它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象。如需将上下文转换为另外一种状态,则需将当前活动的状态对象替换为另外一个代表新状态的对象。采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互

状态可被视为策略的扩展,两者都基于组合机制,通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换;而策略模式中则几乎完全不知道其他策略的存在,相互之间完全独立

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
100
101
102
103
104
105
106
107
108
#include <iostream>
#include <typeinfo>

class Context;

class State
{
protected:
Context *context_;
public:
virtual ~State() {}
void set_context(Context *context)
{
this->context_ = context;
}
virtual void Handle1() = 0;
virtual void Handle2() = 0;
};

class Context
{
private:
State *state_;
public:
Context(State *state) : state_(nullptr)
{
this->TransitionTo(state);
}
~Context()
{
delete state_;
}
void TransitionTo(State *state) // changeState
{
std::cout << "Context: Transition to " << typeid(*state).name() << ".\n";
if (this->state_ != nullptr)
delete this->state_;
this->state_ = state;
this->state_->set_context(this);
}
void Request1()
{
this->state_->Handle1();
}
void Request2()
{
this->state_->Handle2();
}
};

class ConcreteStateA : public State
{
public:
void Handle1() override;

void Handle2() override
{
std::cout << "ConcreteStateA handles request2.\n";
}
};

class ConcreteStateB : public State
{
public:
void Handle1() override {
std::cout << "ConcreteStateB handles request1.\n";
}
void Handle2() override {
std::cout << "ConcreteStateB handles request2.\n";
std::cout << "ConcreteStateB wants to change the state of the context.\n";
this->context_->TransitionTo(new ConcreteStateA);
}
};

void ConcreteStateA::Handle1()
{
{
std::cout << "ConcreteStateA handles request1.\n";
std::cout << "ConcreteStateA wants to change the state of the context.\n";

this->context_->TransitionTo(new ConcreteStateB);
}
}

void ClientCode()
{
Context *context = new Context(new ConcreteStateA);
context->Request1();
context->Request2();
delete context;
}

int main() {
ClientCode();
return 0;
}

/*
Output.txt:执行结果

Context: Transition to class ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to class ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to class ConcreteStateA.
*/

观察者模式 Observer

观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象

拥有一些值得关注的状态的对象通常被称为目标,由于它要将自身的状态改变通知给其他对象,也将其称为发布者(publisher),所有希望关注发布者状态变化的其他对象被称为订阅者(subscribers)

观察者模式建议为发布者类添加订阅机制,让每个对象都能订阅或取消订阅发布者事件流,该机制包括 1)一个用于存储订阅者对象引用的列表成员变量;2)几个用于添加或删除该列表中订阅者的公有方法;无论何时发生了重要的发布者事件,都要遍历订阅者并调用其对象的特定通知方法

所有订阅者都必须实现同样的接口,发布者仅通过该接口与订阅者交互,接口中必须声明通知方法及其参数;甚至可以进一步让所有发布者遵循同样的接口,这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态

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
#pragma once
#include <iostream>
#include <list>
#include <string>

// 观察者(订阅者)的抽象类
class IObserver {
public:
virtual ~IObserver() {};
virtual void Update(const std::string& message_from_subject) = 0; // 得到消息后更新自己的状态
};

// 发布者的抽象类
class ISubject {
public:
virtual ~ISubject() {};
virtual void Attach(IObserver* observer) = 0; // 添加订阅者
virtual void Detach(IObserver* observer) = 0; // 删除订阅者
virtual void Notify() = 0; // 将通知信息发给所有订阅者
};

// 具体发布者
class Subject : public ISubject {
public:
virtual ~Subject();
void Attach(IObserver* observer) override;
void Detach(IObserver* observer) override;
void Notify() override;
void CreateMessage(std::string message); // 传入消息
void HowManyObserver(); // 统计现有的订阅者数量
void SomeBusinessLogic();
private:
std::list<IObserver*> list_observer_; // 存储订阅者对象引用的列表成员变量
std::string message_; // 消息字符串
};

// 具体订阅者
class Observer : public IObserver
{
public:
Observer(Subject& subject);
virtual ~Observer();
void Update(const std::string& message_from_subject) override; // 更新方法,传递发布者消息
void RemoveMeFromTheList(); // 将该订阅者对象从列表中删除(调用 Detach)
void PrintInfo(); // 显示信息
private:
std::string message_from_subject_; // 发布者消息的缓存
Subject& subject_; // 发布者对象的引用
static int static_number_; // 订阅者数量
int number_; // 订阅者编号
};