本文用于个人总结设计模式——C++中的相关知识
类与类之间的关系
- 继承:不多说(空心三角指向父类)
- 关联:A类作为B类的成员变量,可以是单向、双向、自关联。关联的两个对象之间一般是平等的(箭头指向作为成员变量的类)
- 聚合:成员对象是整体的一部分,但是成员对象可以脱离整体对象独立存在。强调二者的松散关系。聚合的两个对象之间一般是平等的(空心菱形指向整体)
- 组合:整体对象可以控制成员对象的生命周期,二者是强关联的。(实心菱形指向整体)
- 依赖:某个类的方法使用另一个类的对象作为参数(虚线+正常箭头指向被作为参数的类)
一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象,聚合则不一样,被聚合的对象可以属于多个聚合对象。
聚合原则
尽量使用聚合,尽量不要使用类继承
聚合表示一种弱拥有关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
聚合原则有助于保持每个类被封装,并集中在单个的任务上。这样类和类继承都会保持较小规模。
依赖倒转原则
- 高层模块不应该依赖低层模块,两个都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
什么意思呢:
- 高层模块:业务层的实现
- 低层模块:底层接口,如封装好的API、动态库等
- 抽象:可以理解为抽象类(但不一定是纯虚),即接口
- 细节:实现细节
- 里氏代换原则:子类类型必须能替换掉它们的父类类型,即可以用父类指针指向子类对象,父类中不能存在子类没有的属性
意思就是:
- 高层模块不应该直接调用低层模块,应当在其中添加抽象类(虚函数),再用抽象类的子类去实现相关调用。这样在相关调用发生变化时,大大减少改动代码量。
- 细节是通过多态在子类重写父类虚函数的时候实现的,需要满足里氏代换原则。
单例模式
懒汉模式的相关问题
可以加锁来解决懒汉模式的线程不安全问题
懒汉模式加锁导致的顺序访问低效问题:使用双重检查锁定:
static TaskQueue* getInstance()
{
if (m_taskQ == nullptr)
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
在这种情况下,底层仍容易出现问题:
由于new
的过程在底层二进制中分为三步实现:申请一块空内存 → 创建对象并写入内存 → 将内存地址传递给指针。但机器在执行指令时会重排指令,若先执行了1、3步才执行2,则导致指针指向了一块没有写入内容的内存,使用该指针的线程就挂了。
怎么解决呢?
使用 C++11 引入的原子变量atomic
这样真的非常麻烦,所以推荐使用下面的方法来实现懒汉模式:
使用静态的局部对象解决线程安全问题
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
static TaskQueue taskQ;
return &taskQ;
}
private:
TaskQueue() = default;
};
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
return 0;
}
C++11 标准引入了对静态局部变量初始化的线程安全保证。具体来说,标准规定:
- 如果多个线程同时访问一个静态局部变量,并且该变量尚未初始化,则只有一个线程会执行初始化,其他线程会等待初始化完成。
- 初始化完成后,所有线程都可以安全地访问该变量。
饿汉模式的线程安全问题
饿汉模式的单例对象不存在线程安全问题
但多线程访问单例对象的内部数据时可能会出现线程安全问题
使用互斥锁保护可能发生线程安全问题的成员变量。
工厂模式
简单工厂模式
// 产品父类(虚)
// 产品子类1
// 产品子类2
// 工厂类(在此new对象)- 输入为想要的产品
// 通过switch-case生产父类指针的子类对象
用多态实现。子类继承基类,基类的析构函数应该是虚函数,这样才能够通过父类指针或引用析构子类的对象。工厂类返回基类类型的指针,保存的是子类对象的地址,所以实际调用的是子类对象中的函数。
switch-case
语句明显违背了封闭原则,仍需要更改。
工厂模式
扩展工厂类,产品父类变成工厂父类,产品子类变成工厂子类
// 工厂父类(虚)
// 工厂子类1(在此new对象)- 不需要参数
// 工厂子类2(在此new对象)
// 产品父类(虚)
// 产品子类1
// 产品子类2
依旧是多态实现。每个工厂返回的还是工厂父类的基类类型。
抽象工厂模式
// 工厂父类1(虚) // 对应产品A
// 工厂子类1-1 // 对应不同的A
// 工厂子类1-2
// 工厂父类2(虚) // 对应产品B
// 工厂子类2-1 // 对应不同的B
// 工厂子类2-2
// 工厂父类3(虚)
// 工厂子类3-1
// 工厂子类3-2
// 抽象工厂类(虚)
// 子工厂类(new 1-1 2-1 3-1的产品)
// 子工厂类(new 1-2 2-2 3-2的产品)
// 子工厂类(new 1-3 2-3 3-3的产品)
建造者模式(生成器模式)
生成器
将建造函数抽离出来,放到“生成器”独立类中。父类生成器中的建造函数应该设置为虚函数。只需要选择需要的生成器步骤并调用,就可以得到满足需求的实例对象。
生成器类内需要提供重置函数(构造函数也会调用这个重置函数),目的是能够使用生成器生成多个对象。
void reset() override {
m_product = new Prodect;
}
同时也提供get函数,将生成的对象送给外部。
主管类
用于定义创建步骤的执行顺序, 程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。
// 目标对象类
// 父类生成器(虚)
// 子类1生成器,拥有多个方法构造不同部件
// 子类2生成器,拥有多个方法构造不同部件
// 主管类(传入父类生成器对象,但实参是子类对象)
// 主管类方法1:调用子类1生成器中的方法1-1 1-2
// 主管类方法2:调用子类1生成器中的方法1-1 1-2 1-3
原型模式
原型模式就是能够复制已有的对象。类的拷贝构造函数只允许复制一个当前类的对象,若想用父类指针拷贝一个子类对象则会失败。原型模式可以通过已有的子类对象克隆父类指针的子类对象。
用克隆函数实现。
// 父类
// 克隆函数(虚)
// 子类1
// 克隆函数(return new 子类1(*this);)
// 子类2
// 克隆函数(返回子类对象)
// 子类对象A
// 父类* B = A->clnoe();
子类的clone()
函数体内部是通过当前子类的拷贝构造函数复制出了一个新的子类对象。
适配器模式
将一个类的接口转换成用户希望的另一个接口。
在使用适配器类为相关的类提供适配服务的时候,如果这个类没有子类就可以让适配器类继承这个类,如果这个类有子类,此时使用继承就不太合适了,建议将适配器类和要被适配的类设置为关联关系。
桥接模式
将抽象部分与实现部分分离,使它们可以独立的变化。为了避免继承带来的子类爆炸。
当设计中有多维的变化,可以相互组合,且都具有很大的变动性,这时如果以某个单独概念为基类来进行继承扩展的话,就会发生子类爆炸。
于是将两个概念都抽象,将继承关系改为聚合关系。
组合模式
能将多个对象组成一个树状结构,用以描述部分——整体的层次关系,使得用户对单个对象和组合对象的使用具有一致性。比如目录树状结构、行政编制、公司组织结构等。
定义抽象节点(集) // 因为该节点同时也管理很多子节点,所以也可以理解为一个集
父节点相关行为
该节点相关行为
子节点相关行为
定义叶子节点(集) : 抽象节点 // 虽然没有子节点了,但也继承抽象节点
该节点相关行为
定义普通管理者节点(集) : 抽象节点
重写父类方法
容器list<抽象节点*> // 存储子节点对象
由于组合模式对应一个树模型,且每个节点的操作方式相同,释放节点的时候可以使用递归函数。
装饰模式
动态的给对象绑定额外的属性,类似于网络通信模型中的各层协议封装、数据加密解密,即不改变数据本质。
定义纯虚抽象类(不可被实例化)
定义子类(这是主体)
定义纯虚装饰类(也继承纯虚抽象类,但不重写全部的纯虚函数)输入是一个纯虚抽象类的指针
包含一个纯虚抽象类的指针变量,用于存放输入的指针
定义子装饰类(重写剩余的纯虚函数)
包含一个最重要使用的功能函数
主函数中一层一层使用装饰类包起来,形成一个"装饰栈"。使用最外层的功能函数即可使用所有的功能函数
+---------------------+
| 子装饰类2 | <- 栈顶
+---------------------+
| 子装饰类1 |
+---------------------+
| 子类 | <- 栈底
+---------------------+
- 子装饰类2 的 func() 被调用;
- 子装饰类2 调用 子装饰类1 的 func();
- 子装饰类1 调用 子类 的func();
外观模式
给很多复杂的子系统提供一个简单的上层接口,这是用户真正关心的功能。
就是用上层接口把复杂子系统接口都执行了,最后输出一个结果就行。
享元模式
当对象涉及的数据内容较多时,区分出静态资源与动态资源,通过数据共享(缓存)让有限的内存可以加载更多的对象。
将内在状态(静态资源,包括可共享的数据、相同的方法等)单独放到一个类中,称为享元类。
当处理动作相同,但细节处理上略有不同时,可以提供一个抽象的基类并在这个类中提供一套虚操作函数,就可以利用重写虚函数来提供不同的处理动作了。
享元工厂
共享对象有很多种,就可以添加一个享元工厂类,专门用来生产这些共享的享元类对象。
代理模式
为其他对象提供一种代理,以控制对这个对象的访问。
代理可以在访问真实对象之前和之后添加一些操作,比如权限检查、延迟加载等等。
所以代理模式突出的是对对象的控制,如:
- 远程代理:控制对远程对象的访问。
- 虚代理:延迟加载对象,直到真正需要时才创建。
- 保护代理:控制对对象的访问权限。
责任链模式
将对象连成一条线,沿着这条链传递请求,直到链上有一个对象将请求处理掉为止。
在责任链模式中,每个节点都有相同的处理动作(处理函数),但节点的权限不同,所以请求可能被处理,也可能没有。
所以他们都有一个共同的抽象管理者基类。
抽象管理基类
set方法 // 用于节点间的连接关系,通常将上级存入一个成员变量
处理函数(权限) = 0;
管理者1类 : 抽象管理基类
处理函数(权限)
switch-case // 能处理的处理,处理不了的传给上级
关键点:处理请求前通过set函数将各个管理者按照等级串联起来
命令模式
将请求转换为一个包含与请求相关的所有信息的独立对象,通过这个转换能够让使用者根据不同的请求操作。
class Receiver // 命令接收者
{
各个处理函数;
}
class AbstractCommand // 抽象命令基类
{
AbstractCommand(Receiver* receiver) : m_receiver(receiver) {}
功能函数 = 0;
}
class CommandA
{
重写功能函数;
}
class Commander(int index, AbstractCommand* cmd)
{
map<int, list<AbstractCommand*>> m_cmdList; // 存储要求信息
}
int main()
{
new...
...
commander->功能函数(index, commandA);
}
命令模式的优势:
- 可以很容易的设计出一个命令队列
- 可以很容易的将命令记录进日志
- 允许接受请求的一方决定是否要否决请求
- 可以很容易的实现对请求的撤销和重做
迭代器模式
能够遍历一个集合对象中的各个元素,而又不暴露该集合底层的表现形式(列表、栈、树、图等)。就是针对某个容器提供对应的操作类,通过迭代器类的封装使对应容器的遍历等操作变得简单,且隐藏了容器的内部细节。
中介者模式
中介者模式可以减少对象之间混乱无序的依赖关系,从而使其耦合松散,限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。中介者对象将系统的网状结构变成以中介者为中心的放射形结构。
抽象中介机构
数据中转函数 = 0;
map<名字, 对象>;
中介机构
重写数据中转函数;
抽象对象类
对象方法;
抽象中介机构* 归属中介 = nullptr
真实对象
中转函数(){归属中介->中转函数}
当一些对象和其他对象紧密耦合以致难以对其进行修改时;当组件因过于依赖其他组件而无法在不同应用中复用时;当为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,都可使用中介者模式。
备忘录模式
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后在需要的时候就可以将该对象恢复到原先保存的状态。
备忘录类
get(); // 得到具体状态m_msg
msg m_msg;
主体类
备忘录类* save() {return new 备忘录类(m_msg)}; // 将状态保存为备忘对象,并通过返回值传出
记录者类
add(); // 添加状态
get(); // 获取状态
map<index, 备忘录类*>;
观察者模式(发布—订阅模式)
定义一种订阅机制,可在对象事件发生时通知所有的观察者对象,使它们能够自动更新。
发布者
需求:
- 添加订阅者
- 删除订阅者
- 发消息给订阅者
class AbstructPublisher
{
void addOb(Observer* ob);
void delOb(Observer* ob);
virtual void notify(xxx msg) = 0; // 广播
list<Observer*> m_list; // 订阅者列表
};
class PublisherA : public AbstructPublisher
{
void notify(xxx msg) override; // m_list中的全部发布
}
订阅者
需求:
- 存储发布者
- 主动取消订阅
- 得到消息后更新当前状态
class AbstructObserver(name, 发布者)
{
构造函数中将自己添加进发布者的m_list;
unSubscribe();
virtual void update(string msg) = 0;
string m_name;
AbstructPublisher* 发布者;
}
class ObserverA
{
void update(string msg) override;
}
当然订阅者和发布者可能是没有子类的,因此也就不需要继承了,这个根据实际情况,具体问题具体分析就可以了。
策略模式
定义一系列算法,将每种算法都放入到独立的类中,在实际操作的时候算法对象可以相互替换
已知:场景A —— 使用策略一;场景B —— 使用策略二;场景C —— 使用策略三。
使用策略模式可以把在什么场景下使用什么策略的判断去除,把处理的逻辑分散到多个不同的策略类中,将复杂逻辑简化。
class AbstractStrategy {
public :
virtual void func1() = 0;
virtual ~AbstractStrategy() {}
};
class StrategyA : public AbstractStrategy {
public:
void func1() override {
...
}
};
class StrategyB : public AbstractStrategy {
public:
void func1() override {
...
}
};
class user { // 策略使用者
public:
void selete(level) {
switch(level)
{
case level::A:
mStrategy = new StrategyA;
break;
case level::B:
mStrategy = new StrategyB;
break;
}
mStrategy->func1();
}
~user() {}
private:
AbstractStrategy* mStrategy = nullptr;
}
状态模式
状态模式就是在一个类的内部会有多种状态的变化,因为状态变化从而导致其行为的改变,在类的外部看上去这个类就像是自身发生了改变一样。
状态模式和策略模式有点像,但不同点是:策略模式的case选项无关联,但状态模式的case可以独立也可以有一定联系。
class Owner;
class AbstractState { // 状态抽象类
public:
virtual void func(Owner* owner) = 0;
virtual ~AbstractState() {}
};
class StateA : public AbstactState {
public:
void func(Owner* owner) override;
};
class StateB : public AbstactState {
public:
void func(Owner* owner) override;
};
class Owner { // 状态拥有者
public:
Owner() {
mState = new StateA;
}
void func() {
mState -> func(this);
}
void setState(AbstactState* state)
{
if (mState != nullptr) {delete mState;}
mState = state;
}
~Owner() {delete mState;}
private:
AbstractState* mState = nullptr;
};
// 一般来说,Owner类中还拥有一个m_clock变量,且各个State的func内还包含很多不同行为
// 时间变化,对象根据自身状态进行不同行为。
如果对象需要根据当前自身状态进行不同的行为, 同时状态的数量非常多且与状态相关的代码会频繁变更或者类对象在改变自身行为时需要使用大量的条件语句时,可使用状态模式。
模板方法模式
先定义一个基类,在基类中把与需求相关的所有操作函数全部作为虚函数定义出来,然后在这个基类的各个子类中重写父类的虚函数,这样子类基于父类的架构使自己有了和其他兄弟类不一样的行为。
模板方法这种设计模式是对多态的典型应用。
class AbstractTemplate {
public:
virtual void func1() = 0;
virtual void func2() = 0;
virtual void func3() = 0;
virtual void func4();
}
class TemplateA {
public:
void func1() override {}
void func2() override {}
void func3() override {}
}
class TemplateB {
public:
void func1() override {}
void func2() override {}
void func3() override {}
void func4() override {}
}
访问者模式
当对象很多,但对象对应的状态(能使用的算法)很少时,避免出现很多相同的冗余代码,将算法与其所作用的对象隔离开来,通过被分离出的算法来访问对应的对象。
对于成员,提供一个接受访问的函数:
class AbstractMember{ // 抽象成员
public:
AbstractMember(string name) : m_name(name) {}
string getName() {
return m_name;
}
// 接受状态对象的访问
virtual void accept(AbstractAction* action) = 0;
virtual ~AbstractMember() {}
protected:
string m_name;
};
以此定义子类成员,成员类中都是成员自身的一些属性信息,行为都被分离出去了。
class AMember : public AbstractMember
{
public:
AbstractMember::AbstractMember;
// 在调用成员类的accept()函数的时候,将具体地行为状态通过参数传递给了成员。
void accept(AbstractAction* action) override {
// 在accept()函数中通过行为状态对象调用行为函数的时候,将当前成员对象传递给了状态对象。
action->ADoing(this);
}
};
class BMember : public AbstractMember
{
public:
AbstractMember::AbstractMember;
void accept(AbstractAction* action) override {
action->BDoing(this);
}
};
行为基类:
class AMember;
class BMember;
class AbstractAction
{
public:
// 访问成员A
virtual void ADoing(AMember* mem) = 0;
virtual void BDoing(ABember* mem) = 0;
virtual ~AbstractAction() {}
};
行为子类:
class AAct : public AbstractAction
{
public:
void ADoing(AMember* mem) override;
void BDoing(BMember* mem) override;
void func1();
void func2();
};
class BAct : public AbstractAction
{
public:
void ADoing(AMember* mem) override;
void BDoing(BMember* mem) override;
void func3();
void func4();
};
访问者不是常用的设计模式, 因为它不仅复杂, 应用范围也比较狭窄。