首页 > 常见设计模式(单例,工厂,抽象工厂,观察者(C++))
头像
刘刘刘三样
发布于 2023-03-05 16:21 四川
+ 关注

常见设计模式(单例,工厂,抽象工厂,观察者(C++))

其实我也不知道常用的是不是这几个,先这么准备着吧,程序代码都是用c++写的,所以也会涉及一些C++的特性,参考了苏老师和自己找的一些博客。

0.设计模式3原则

0.1单一职责原则

一个类做一个事,如果一个类承担职责过多,就等于把这些职责耦合到了一起,增加了类的耦合性,可能会削弱或者抑制这个类完成其他职责的能力。

0.2开放封闭原则(核心)

开放 – 封闭原则说的是软件实体(类、模块、函数等)可以扩展,但是不可以修改。也就是说对于扩展是开放的,对于修改是封闭的。

下面在简单工厂模式会举例

0.3依赖倒转原则(多态的典型应用)

1.高层模块不应该依赖低层模块,两个都应该依赖抽象。

高层模块:可以理解为上层应用,就是业务层的实现

低层模块:可以理解为底层接口,比如封装好的 API、动态库等

抽象:指的就是抽象类或者接口,在 C++ 中没有接口,只有抽象类

1.单例模式

在一个项目中,全局范围内,某个类的实例有且仅有1个,通过这个唯一实例向其他模块提供数据的全局访问。

典型应用:任务队列。

1.0 static

在此先介绍一个概念,static,如何通过这个唯一实例向其他模块提供数据全局访问?

最容易想到的方法是定义为全局的变量,但定义一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅只受此函数控制)。static 关键字则可以很好的解决这个问题。

在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。

1.第一条也是最重要的一条:隐藏

2.保持变量内容的持久

3.默认初始化为 0

静态成员的定义或声明要加个关键 static。

静态成员可以通过双冒号来使用即 <类名>::<静态成员名>

static详细讲解:https://www.runoob.com/w3cnote/cpp-static-usage.html

为了保证有且仅有一个,涉及到的函数:假如A是这个类

1.构造函数(A)处理方法:

由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为私有的。

(=default) 指定无参构造为默认函数

静态成员是在类外部初始化的,在定义这个单例类的时候,就已经把这个静态的单例对象创建出来了,可以使用一个类内public函数封装这个数据,获取的时候调用这个函数。

2.拷贝构造函数(A(const A &obj)) 处理方法 :私有化或者禁用(=delete)

3.拷贝赋值操作符重载函数(A& operator=(const A& obf)) 处理方法:私有化或者禁用(=delete)

通过static和禁用拷贝构造,禁用拷贝赋值操作符这两个操作,完成了某个类的实例有且仅有1个,通过这个唯一实例向其他模块提供数据的全局访问。

1.1饿汉模式

在类加载的时候就立刻进行·实例化,得到了一个唯一的可用对象。

类的静态成员变量在使用之前必须在类的外部进行初始化才能使用。

// 饿汉模式
class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        return m_taskQ;
    }
private:
    TaskQueue() = default;
    static TaskQueue* m_taskQ;
};
// 静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;

int main()
{
    TaskQueue* obj = TaskQueue::getInstance();
}


1.2懒汉模式

在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化。

// 懒汉模式
class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        if(m_taskQ == nullptr)
        {
            m_taskQ = new TaskQueue;
        }
        return m_taskQ;
    }
private:
    TaskQueue() = default;
    static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;

在调用 getInstance() 函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance() 函数,在这个函数内部每个线程都会 new 出一个实例对象。此时,这个任务队列类的实例对象不是一个而是 3 个,很显然这与单例模式的定义是相悖的。无法保证线程安全

1.3懒汉模式线程安全

双重检查锁定

static TaskQueue* getInstance()
    {
        if (m_taskQ == nullptr)//外层加锁,当任务队列没被创建时,才会加锁,不然无论是否创建就加锁,
		//在重负载情况下,可能导致响应缓慢
        {
            m_mutex.lock();
            if (m_taskQ == nullptr)//内层加锁,上锁后再创建,保证线程安全
            {
                m_taskQ = new TaskQueue;
            }
            m_mutex.unlock();
        }
        return m_taskQ;
    }


静态局部变量更简单地解决了线程安全问题(C++ 11的标准,具体没研究明白)

1.4总结

懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。

2.工厂模式

2.1简单工厂模式

这里也是参考苏老师的一个例子:

海贼王中人造恶魔果实smile的制造工厂:

首先我们可以制造一个抽象果实类,然后创建羊,蝙蝠,狮子等果实继承这个抽象果实类,再有强枚举搭配switch创建一个工厂类,让工厂类实现创建子类果实实例的过程,示例图如下:

但是这里违背了一个原则:开放封闭原则,假如此时我们重新扩展了一个老虎类,它可以继承抽象果实类(开放),

但是此时你必须修改工厂的代码(违反了封闭的原则)用来添加老虎类对象的实例化,于是出现了工厂模式

2.2工厂模式

(这里强调一个定义,图示所示抽象XX是因为该类并未有具体的实现,只是代表了某种形式,所以叫抽象XX,这里面的构造函数和析构函数,虚或者纯虚都无所谓。

而C++里面的抽象类:凡是包含纯虚函数的类都是抽象类)

这里就要把工厂也变成抽象类了,方便扩展,流程图如下:

两个父类,创建子类后,X工厂类用来创建X。(此处用到了继承和多态)

这里强调一个有关C++的情况,就是抽象工厂类里面的的构造函数和析构函数:

构造函数是虚函数或者纯虚函数都行,主要看析构函数

写法一:虚析构函数

虚函数:指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,实现动态绑定。

C++中父类析构函数加上virtual是为了防止内存泄漏。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。

那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

写法二:纯虚析构函数

父类析构函数可以是纯虚函数,但是必须实例化,否则子类继承后无法实例化

子类的实例化对象析构的时候,先调用子类的析构函数,然后再调用父类的析构函数

3.抽象工厂模式

假设想造船,有三种型号可供选择:

比如新建一个基础型船,就要先创建一个基础型船工厂,然后工厂会直接new一个船,里面会new基础船体,new基础武器,new基础动力,然后新船就被构建了。在这里为什么符合开放封闭,比如我们想做超级豪华船,就新创建一个超级豪华船工厂类,然后创建对应的超级豪华船体,武器,动力,然后船(ship)就被new出来了。

总结:

简单工厂模式不能遵守开放-封闭原则,工厂和抽象工厂模式可以

简单工厂模式只有一个工厂类,工厂和抽象工厂有多个工厂类

工厂模式创建的产品对象相对简单,抽象工厂模式创建的产品对象相对复杂

4.观察者模式

观察者模式允许我们定义一种订阅机制,可在对象事件发生时,发布者对象通知所有的观察者对象,使它们能够自动更新。观察者模式还有另外一个名字叫做 “发布 - 订阅” 模式。

4.1发布者

同样参考苏老师的例子,海贼王太经典了,哈哈哈

摩根斯的新闻社是一个报纸发行机构,内容大多是与当前的世界格局和各地发生的事件有关,其他人也可以发报纸,为了避免竞争可以更换一下题材,比如海贼们的八卦新闻(八卦报社),不管是哪一类都需要满足以下的需求:

添加订阅者,将所有的订阅者存储起来(attach)

删除订阅者,将其从订阅者列表中删除(detach)

通过发布者notify函数更新消息->使用订阅者的update函数将消息发送给订阅者(发通知)

// 订阅者列表 list<Observer*> m_list;

4.2订阅者

虽然在海贼王中尾田构建的是一个强者的世界,但是还是有温情存在的,路飞虽然不知道自己的老爹长啥样、是干什么的,但是知道自己还有个儿子的龙还是一直在默默关注着路飞。另外,还有把未来希望寄托在路飞身上的香克斯,也一直在关注着路飞的成长。所以观察者这个角色可能不是一个人,可能是几个或者是一个群体,但他们的行为是一致的,所以我们可以给所有的观察者定义一个抽象的基类。满足以下需求:

通过订发布者变量把自己添加进订阅者列表

通过订发布者变量把自己从订阅者列表删除

订阅者update函数表示收到发布者发送的信息

4.3流程图

全部评论

(6) 回帖
加载中...
话题 回帖