简历描述

熟悉常用的设计模式,如单例模式、工厂模式、命令模式和装饰模式等。

回答思路

设计模式没什么底层而言,介绍的时候,形象为大,你最好是能关联你项目中用到的东西或者是某常用的语言的某功能用的来举个例子,这才能让人眼前一亮,表示你已经融汇贯通~~。
所以本文带你用通俗的语言来学习,看完之后
你就会悟到为什么简历书写的介绍要写以上四个设计模式例子作为比如啦!!!!

设计模式三原则

保证程序的可扩展性、可维护性和可读性

单一职责原则

对一个类而言,应该仅有一个引起它变化的原因
软件设计真正要做的事情就是,发现根据需求发现职责,并把这些职责进行分离,添加新的类,给当前类减负,越是这样项目才越容易维护。

开放封闭原则

软件实体(类、模块、函数等)可以扩展,但是不可以修改**(对于扩展是开放的,对于修改是封闭的)**
需要在这些位置创建抽象类来隔离以后发生的这些同类变化(其实就是对多态的应用,创建新的子类并重写父类虚函数,用以更新处理动作)。

此处的抽象类,其实并不等价与C++中完全意义上是抽象类(需要有纯虚函数),这里所说的抽象类只需要包含虚函数(纯虚函或非纯虚函数)能够实现多态即可。

依赖倒转原则

1.高层模块(可以理解为上层应用,就是业务层的实现)
不应该依赖
低层模块(可以理解为底层接口,比如封装好的API、动态库等),
两个都应该依赖抽象(指的就是抽象类或者接口,在C++中没有接口,只有抽象类)。
2.抽象不应该依赖细节,细节应该依赖抽象。

  • 里氏代换原则

子类类型必须能够替换掉它们的父类类型。

依赖倒转原则如下图:Ⅰ—>Ⅱ
抽象类中提供的接口是固定不变的

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

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

在这里插入图片描述

在这里插入图片描述

以下所有模式归属为创建型模式

单例模式(全局唯一)

在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。

饿汉模式(出现即实例化)

饿汉模式就是在类加载的时候立刻进行实例化。
当使用者通过getInstance()获取这个单例对象的时候,它已经被准备好了。
注意事项:类的静态成员变量在使用之前必须在类的外部进行初始化才能使用。

懒汉模式(用才实例化,多进程易打架!)

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

懒汉模式的问题以及解决思路

线程安全问题:n个线程同时去获取实例,就会产生n个对象
这个时候想到的方式就肯定是加锁
加锁问题:第一次肯定没办法,但是第一次以后,明明已经创建出来了,但每次还是要静态条件,效率低下。没事那我们搞个if双重检查锁定,如果是第一次,就加锁,不然不加
双重检查锁定的问题:对应的机器指令可能会被重新排序导致指针指向的内存却没有被初始化(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现),这个可用C++的原子变量去解决
C++的原子变量去解决代码如下:

class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        TaskQueue* queue = m_taskQ.load();  
        if (queue == nullptr)
        {
            // m_mutex.lock();  // 加锁: 方式1
            lock_guard<mutex> locker(m_mutex);  // 加锁: 方式2
            queue = m_taskQ.load();
            if (queue == nullptr)
            {
                queue = new TaskQueue;
                m_taskQ.store(queue);
            }
            // m_mutex.unlock();
        }
        return queue;
    }

    void print()
    {
        cout << "hello, world!!!" << endl;
    }
private:
    TaskQueue() = default;
    static atomic<TaskQueue*> m_taskQ;
    static mutex m_mutex;
};
atomic<TaskQueue*> TaskQueue::m_taskQ;
mutex TaskQueue::m_mutex;

int main()
{
    TaskQueue* queue = TaskQueue::getInstance();
    queue->print();
    return 0;
}

上面代码中使用原子变量atomic的store() 方法来存储单例对象,使用load()
方法来加载单例对象。在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst(顺序原子操作 -
sequentially consistent),使用顺序约束原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data
races),不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。

  • 一步到位?静态局部变量
class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        static TaskQueue taskQ;
        return &taskQ;
    }
    void print()
    {
        cout << "hello, world!!!" << endl;
    }

private:
    TaskQueue() = default;
};

int main()
{
    TaskQueue* queue = TaskQueue::getInstance();
    queue->print();
    return 0;
}

在程序的第 9、10
行定义了一个静态局部队列对象,并且将这个对象作为了唯一的单例实例。使用这种方式之所以是线程安全的,是因为在C++11标准中有如下规定,并且这个操作是在编译时由编译器保证的:

如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。

懒汉与饿汉对比

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

工厂模式

不论使用哪种工厂模式其主要目的都是实现类与类之间的解耦合,这样我们在创建对象的时候就变成了拿来主义,使程序更加便于维护。

简单工厂模式(面向过程的工厂)

简单工厂模式相关类的创建和使用步骤如下:

创建一个新的类, 可以将这个类称之为工厂类。对于简单工厂模式来说,需要的工厂类只有一个。
在这个工厂类中添加一个公共的成员函数,通过这个函数来创建我们需要的对象,关于这个函数一般将其称之为工厂函数。(有很多switch case)
关于使用,首先创建一个工厂类对象,然后通过这个对象调用工厂函数,这样就可以生产出一个指定类型的实例对象了。

  • 但是有一个问题是违反了设计模式三大原则之一的封闭原则,用工厂模式解决

工厂模式(面向工厂对象的简单工厂)

一个基类,包含一个虚工厂函数,用于实现多态。
多个子类,重写父类的工厂函数。每个子工厂类负责生产一种恶魔果实,这相当于再次解耦,将工厂类的职责再次拆分、细化,如果要生产新品种的恶魔果实,那么只需要添加对应的工厂类,无需修改原有的代码。

这样每个工厂类其实都不复杂,在每个子工厂类中也只是重写了父类的工厂方法而已,每个子工厂类生产一种恶魔果实,但是工厂函数的返回值确是恶魔果实类的基类类型,相当于是使用父类指针指向了子类对象,此处也是用到了多态。通过这样的处理,工厂函数也就不再需要参数了。

抽象工厂模式(面向工厂以及所有可变零件对象的复杂工厂)

提供一个抽象的基类,然后在它的子类中完成各种型号的船的组装,每个子类对应的就是一条生产线。
抽象工厂模式适用于比较复杂的多变的业务场景,总体上就是给一系列功能相同但是属性会发生变化的组件(如:船体材料、武器系统、动力系统)添加一个抽象类,这样就可以非常方便地进行后续的拓展,再搭配工厂类就可以创建出我们需要的对象了。

简单工厂模式、工厂模式和抽象工厂模式的区别

简单工厂模式不能遵守开放-封闭原则,工厂和抽象工厂模式可以
简单工厂模式只有一个工厂类,工厂和抽象工厂有多个工厂类
工厂模式创建的产品对象相对简单,抽象工厂模式创建的产品对象相对复杂
工厂模式创建的对象对应的类不需要提供抽象类【这产品类组件中没有可变因素】
抽象工厂模式创建的对象对应的类有抽象的基类【这个产品类组件中有可变因素】

生成器模式(也叫建造者模式:造产品以指定流程为主)

  • 规定一个固定的建筑流程往里面填不同的材料来实现不同的产品

化繁为简,逐个击破。也就是分步骤创建复杂的对象,并且允许使用相同的代码生成不同类型和形式的对象

比如你要造一栋房子,有地基、墙、屋顶。你可以用同样的步骤(先打地基、再砌墙、最后盖屋顶),但用不同的材料(木头、砖、钢筋),最后就能造出不同风格的房子。这就是建造者模式的思想。

原型模式(让父类可以生成子类)

原型模式就是能够复制已有的对象,而又无需使代码依赖它们所属的类。换种说法,就是通过已有对象克隆出另一个新的对象,并且克隆这个对象不需要使用构造函数。

一个拷贝构造函数就能搞定的事情为啥还要搞出一种设计模式呢?

克隆可能会在父类和子类之间进行,并且可能是动态的,很明显通过父类的拷贝构造函数无法实现对子类对象的拷贝,其实这就是一个多态,我们需要给父类提供一个克隆函数并且是一个虚函数。
比如:

#include <iostream>
using namespace std;

class GermaSoldier
{
public:
//纯虚函数的接口
    virtual GermaSoldier* clone() = 0;
    virtual string whoAmI() = 0;
    virtual ~GermaSoldier() {}
};

class Soldier66 : public GermaSoldier
{
public:
    GermaSoldier* clone() override
    {
        return new Soldier66(*this);
    }
    string whoAmI() override
    {
        return string("我是杰尔马66的超级士兵!!!");
    }
};

class Soldier67 : public GermaSoldier
{
public:
    GermaSoldier* clone()
    {
        return new Soldier67(*this);
    }
    string whoAmI() override
    {
        return string("我是杰尔马67的超级士兵!!!");
    }
};

int main()
{
    GermaSoldier* obj = new Soldier66;
    GermaSoldier* soldier = obj->clone();
    cout << soldier->whoAmI() << endl;
    delete soldier;
    delete obj;

    obj = new Soldier67;
    soldier = obj->clone();
    cout << soldier->whoAmI() << endl;
    delete soldier;
    delete obj;
}

第41行通过父类指针克隆了子类Soldier66的对象
第47行通过父类指针克隆了子类Soldier67的对象
在这两个士兵子类的clone()函数体内部是通过当前子类的拷贝构造函数复制出了一个新的子类对象。

以下所有模式归属为结构型模式

适配器模式(找个翻译)

将一个类的接口转换成用户希望的另一个接口,使不兼容的对象能够相互配合并一起工作,这种模式就叫适配器模式。

  • 在使用适配器类为相关的类提供适配服务的时候,如果这个类没有子类就可以让适配器类继承这个类,如果这个类有子类,此时使用继承就不太合适了,建议将适配器类和要被适配的类设置为关联关系。

桥接模式(all继承NO!组合+继承YSE!)

  • 将抽象和现实通过组合连接,而不是一味的继承

将抽象部分和它的实现部分分离,使它们可以独立的变化,这种处理模式就是桥接模式。也就是说,原本“抽象”和“实现”是绑在一起的,现在用“桥”把它们连接起来,这样你可以单独扩展“抽象”或者“实现”,互不影响。

海贼团和船员的例子 第一种做法:每个船员都是海贼团的子类。这样一旦有新船员,就要新建一个子类,船员和海贼团死死绑在一起,扩展很麻烦。
第二种做法(桥接模式):海贼团和船员分开,海贼团只是一个“壳”,里面通过“聚合”方式(比如有一个成员变量是团队/船员列表)来管理船员。这样,海贼团和船员可以各自独立扩展,互不影响。
遥控器和家电的例子
电视机和遥控器是两个独立的体系。你可以有不同品牌的电视,也可以有不同功能的遥控器。桥接模式让你可以随意组合:A品牌电视+A遥控器,B品牌电视+B遥控器,互不影响。

  • 当一个类有多个变化维度(比如“海贼团类型”和“船员类型”),而且这些维度都需要独立扩展时,就可以用桥接模式。
  • 这样可以避免类爆炸(比如每加一个新船员都要新建一个子类),让代码更灵活、易维护。

组合模式(Linux 的树状目录结构 )

能将多个对象组成一个树状结构,用以描述部分—整体的层次关系,使得用户对单个对象和组合对象的使用具有一致性,这样的结构性设计模式叫做组合模式。

Linux 的树状目录结构
国家的行政区划分(省级、地级、县级、乡级)
解放军编制(军、师、旅、团、营、连、排、班)
公司的组织结构(树状)

在这里插入图片描述

装饰模式(人+衣服=(穿衣服的)人!=衣服人)

动态的给一个对象绑定额外的属性(比如:能力、职责、功能等),在原有行为之上进行拓展,并不会改变该行为。

  • 在进行网络通信的时候

,数据是基于IOS七层或四层网络模型(某些层合并之后就是四层模型)进行传输,通过下图可得知从应用层到物理层,数据每向下走一层就会被封装一层,最后将封装好的数据以比特流的方式发送给接收端。封装之后数据只是变得更复杂了, 并没有改变它是数据的本质。

A端和B端进行网络通信,默认数据是在网络环境中裸奔的,如果想要对数据进行装饰(也就是封装)就可以在发送数据之前对其加密,接收端收到数据之后对其解密。加解密是对数据的装饰,但是没有改变数据的本质。
在这里插入图片描述

  • 平时都是穿长衣长裤

,疫情来袭之后穿上了防护服。防护服是对人的装饰,没有改变本体是人的本质。

//各种果实 来源于一个恶魔果实抽象类(他继承了战士类)
int main()
{
    Teach* teach = new Teach("马歇尔·D·蒂奇");
    DarkFruit* dark = new DarkFruit;
    QuakeFruit* quake = new QuakeFruit;
    PieFruit* pie = new PieFruit;
    // 黑胡子吃了暗暗果实
    dark->enchantment(teach);
    // 黑胡子又吃了震震果实
    quake->enchantment(dark);
    // 黑胡子又吃了大饼果实
    pie->enchantment(quake);
    // 战斗
    pie->fight();
    delete teach;
    delete dark;
    delete quake;
    delete pie;
    return 0;
}

外观模式(提供让用户傻瓜式操作的接口)

外观模式就是给很多复杂的子系统提供一个简单的上层接口,并在这些接口中包含用户真正关心的功能。

通过电商平台下单,就可以收到自己需要的商品。
上层接口:用于下单、查看物流信息、确认收货的用户界面
底层模块:供货商、仓库、包装、送货、支付处理、售后等
购买基金
上层接口:可供用户操作的UI界面
底层模块:基金分类、购买股票、购买国债、购买企业债 音频转换工具
上层接口:可供用户操作的UI界面
底层模块:MP3编解码模块、FALC编解码模块、APE编解码模块、等。。。
智能家居系统
上层接口:可供用户操作的UI界面
底层模块:电视、灯、热水器、空调、路由器、。。。。

外观模式是一个很重要、平时也经常使用的设计模式,其核心思想就是化繁为简,封装底层逻辑,将使用者真正关心的功能通过上层接口呈现出来。

享元模式(构造共享对象,节省系统内存资源,通常会用有参数列表的构造函数的方法)

假设游戏中制作一个炮弹需要的数据有四部分数据,其中有些属性是动态的,有些属性是静态的: 静态资源:精灵图和渲染的颜色 动态属性:坐标和速度

享元模式就是摒弃了在每个对象中都保存所有的数据的这种方式,通过数据共享(缓存)让有限的内存可以加载更多的对象。

  • 类似线程池复用线程
  • 享元模式中共享的是对象,共享内存也可以将其称之为缓存。

使用享元模式一般建议将内在状态和外在状态分离,将内在状态单独放到一个类中,这种类我们可以将其称之为享元类。

享元子类

注意:不贴代码了,因为把以下具体的类名只代表抽象的概念ABC就完全足够去理解这个事儿了
一般享元数据都是共享的,但是这里的UniqueBomb 类,它虽然是享元类的子类,但这个类的实例对象却是不共享数据(假设每个彩蛋的外观和用途都是不同的),表面看起来矛盾,但是也合乎常理。尽管我们大部分时间都需要共享对象来降低内存的损耗,但在个别时候也有可能不需要共享的数据,此时 UniqueBomb 子类就有存在的必要了,它可以帮助我们解决那些不需要共享对象场景下的问题,使用这种处理方式对应的操作流程是无需做出任何改变的。 如果有上述的需求,就可以给享元类提供一个基类。

享元工厂

假设炮弹有很多种型号,此时就需要有很多张精灵图,也就是说SharedBombBody 类型的对象对应也应该有很多个,此时我们就可以再添加一个享元工厂类,专门用来生产这些共享的享元类对象。

享元模式与单例模式的对比

享元模式中的享元类可以有子类也可以没有
享元模式中可以添加享元工厂也可以不添加
享元工厂的作用和单例模式类似,但二者的关注点略有不同
单例模式关注的是类的对象有且只有一个
享元工厂关注的是某个实例对象是否可以共享

代理模式(方便管理被代理的对象的工作的时机,但是并没有改变被代理的对象的行为)

为其他对象提供一种代理,以控制对这个对象的访问。

通过信用卡、微信、支付宝等代替现金支付
开发一套对接数据库服务器的接口并提供给客户使用,用于提高服务器的访问效率
跑腿小哥代替大聪明给异地的女盆友送花。
通过VPN架梯子访问外网。

  • 我们可以在代理类中有效的管理被代理的对象的工作的时机,但是并没有改变被代理的对象的行为。

如果使用代理模式,不能改变所代理的类的接口,使用代理模式的目的是为了加强控制。

以下设计模式都属于行为模式

责任链模式(为了一关关所有)

  • 类似在中国(区–>中央)层层上报

将对象连成一条链,并沿着这条链传递请求,直到链上有一个对象将请求处理掉为止,这种处理数据的模式叫做责任链模式。

  • 使用这种模式有一个好处:处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

在这里插入图片描述

公司的OA审批系统

移动、联通语音服务器系统

> GUI操作界面!!!!(人人都知道的!!!)
在这里插入图片描述

  • 单向链

责任链模式就是将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存下一个处理者。 除了处理请求外, 处理者还负责沿着链传递请求, 请求会在链上移动, 直至所有处理者都有机会对其进行处理。

它其实还可以有其他的变种:

  • 双向链:将当前节点的后继对象和前驱对象都记录下来

  • 树状链:当前节点可以有多个子节点,也就是多个后继节点,可以将它们保存到一个STL容器中。

命令模式(deepchat项目里面封装的http对象)

比如说chat通信项目
客户端是命令的发送者
服务器是命令的接收者
封装的http对象一个个排队构成了任务队列
如果没有这个对象
那就需要一个东西频繁穿梭在二者之间哪个人要怎么通信也容易弄混

命令模式就是将请求转换为一个包含与请求相关的所有信息的独立对象,通过这个转换能够让使用者根据不同的请求将客户参数化、 延迟请求执行或将请求放入队列中或记录请求日志, 且能实现可撤销操作

int main()
{
    CookerZeff* cooker = new CookerZeff;
    WaiterLuffy* luffy = new WaiterLuffy;

    YXRSCommand* yxrs = new YXRSCommand(cooker);
    GBJDCommand* gbjd = new GBJDCommand(cooker);
    DSXCommand* dsx = new DSXCommand(cooker);
    HSPGCommand* hspg = new HSPGCommand(cooker);

    cout << "=================== 开始点餐 ===================" << endl;
    luffy->setOrder(1, yxrs);
    luffy->setOrder(1, dsx);
    luffy->setOrder(1, gbjd);
    luffy->setOrder(1, hspg);
    luffy->setOrder(2, dsx);
    luffy->setOrder(2, gbjd);
    luffy->setOrder(2, hspg);
    cout << "=================== 撤销订单 ===================" << endl;
    luffy->cancelOrder(1, dsx);
    cout << "=================== 开始烹饪 ===================" << endl;
    luffy->notify(1);
    luffy->notify(2);
    cout << "=================== 结账 ===================" << endl;
    luffy->checkOut(1);
    luffy->checkOut(2);

    return 0;
}
//可以看到都操作的是luffy

命令模式最大的特点:松耦合设计

  • 使用这种模式可以很容易地设计出一个命令队列
  • 可以很容易的将命令记录到日志中
  • 允许接收请求的一方决定是否要否决请求
  • 可以很容易的实现对请求的撤销和重做

解释QT信号和槽的特点

  • 松散耦合!
  • 发出信号的人不关心谁要处理信号,接收信号的人不关心谁发的信号!!!
  • 并不完全等同于命令模式,但两者有相似之处。你恍然大悟了吗?!
  • 命令模式:明确把“请求”封装成一个对象(命令对象),可以保存、撤销、排队等。例如:Command类有execute()方法,调用者只需调用execute(),不用关心具体实现。
  • 信号和槽:更像是“事件-响应”机制,信号发出后,所有连接的槽自动响应。它没有专门的“命令对象”,也不直接支持撤销、排队等功能。

迭代器模式(在暴露具体数据结构的情况下进行遍历)

提供一种解决方案使其能够顺序访问一个集合对象中的各个元素,而又不暴露该集合底层的表现形式(列表、栈、树、图等),这种行为设计模式就叫迭代器模式。

上课点名
上体育课报数
坐火车查票
躲在被窝里数钱
STL容器的迭代器

  • 迭代器模式是一个很经典的模式。所以没必要重复的去造这个轮子,成型的类库都非常好的实现了迭代器模式,在使用这些类库提供的容器时,并不需要我们亲自去实现对应的迭代器,比如STL

中介者模式(建设交通枢纽的作用:系统的结构体不会因为新对象的引入造成大量的修改工作)

中介者模式可以减少对象之间混乱无序的依赖关系,从而使其耦合松散,限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。

如果不使用中介者模式,各个国家之间的关系就是一个网状结构,关系错综复杂,这样的系统也很难容易维护。有了中介者对象,可以将系统的网状结构变成以中介者为中心的放射形结构。每个具体的对象不再通过直接的联系与另一对象发生相互作用,而是通过中介者对象与另一个对象发生相互作用。

  • 中介者对象的设计,使得系统的结构体不会因为新对象的引入造成大量的修改工作。

现实生活中关于中介者模式的应用也有很多,以下这些都是中介者:

联合国

各种中介机构:房产中介、婚恋中介、出国留学中介

十字路口指挥交通的信号灯或者交警

机场协调各架次飞机起降的塔台

//让四个国家加入组织,通过中介者对象进行交流
int main()
{
    // 世界政府
    WorldGovt* world = new WorldGovt;
    Alabasta* alaba = new Alabasta(world);
    Dressrosa* dresa = new Dressrosa(world);
    // 世界政府添加成员
    world->addMember(alaba);
    world->addMember(dresa);
    // 世界政府成员发声
    alaba->declare("德雷斯罗萨倒卖军火, 搞得我国连年打仗, 必须给个说法!!!", dresa->getName());
    dresa->declare("天龙人都和我多弗朗明哥做生意, 你算老几, 呸!!!", alaba->getName());
    cout << "======================================" << endl;
    // 革命军
    GeMingArmy* geming = new GeMingArmy;
    Lulusia* lulu = new Lulusia(geming);
    Kamabaka* kama = new Kamabaka(geming);
    geming->addMember(lulu);
    geming->addMember(kama);
    lulu->declare("我草, 我的国家被伊姆毁灭了!!!", lulu->getName());

    delete world;
    delete alaba;
    delete dresa;
    delete geming;
    delete lulu;
    delete kama;
    return 0;
}

当一些对象和其他对象紧密耦合以致难以对其进行修改时;当组件因过于依赖其他组件而无法在不同应用中复用时;当为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,都可使用中介者模式。

备忘录模式(不破坏封装性下恢复原来的状态)

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后在需要的时候就可以将该对象恢复到原先保存的状态。

各种经典著作,比如:资治通鉴,史记、二十四史、黄帝内经、论语、道德经

数据库的事务(出现错误时可以进行数据的回滚操作)

游戏的进度存档

文本编辑器

InnoDB实现隔离性的方法——MVCC

MVCC就是通过保存数据的多个历史版本,让读写互不干扰,大大提升了数据库的并发性能。
在这里插入图片描述

备忘录模式中的三要素:事件的主体、事件的内容、事件的记录者

备忘录模式的应用场景

  • 当你需要创建对象状态快照来恢复其之前的状态时(比如游戏存档、文本编辑器)
  • 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时(主要是为了保护数据的安全性,不允许篡改)

观察者模式(对象A和对象B变化相关联并且不破坏封装性)

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

使用的社交软件,当关注的博主更新了内容,会收到提示信息
购买的商品被送到菜鸟驿站,会收到驿站发送的提示信息
订阅了报刊,每天/每月都会收到新的报纸或者杂志

当一个对象的状态发生变化,并且需要改变其它对象的时候;或者当应用中一些对象必须观察其它对象的时候可以使用观察者模式。

构造一个观察模式

定义发布者(抽象类)
定义发布者1:public 发布者
定义发布者2:public 发布者

定义订阅者(抽象类)
定义订阅者1:public 订阅者
定义订阅者2:public 订阅者

发布者1–>notify(订阅者1,‘XXX’)
发布者1–>notify(订阅者2,‘XXX’)
发布者2–>notify(订阅者1,‘XXX’)
发布者2–>notify(订阅者2,‘XXX’)

int main()
{
    Morgans* ms = new Morgans;
    Gossip* gossip = new Gossip;
    Dragon* dragon = new Dragon("蒙奇·D·龙", ms);
    Shanks* shanks = new Shanks("香克斯", ms);
    Bartolomeo* barto = new Bartolomeo("巴托洛米奥", gossip);
    ms->notify("蒙奇·D·路飞成为新世界的新的四皇之一, 赏金30亿贝里!!!");
    cout << "======================================" << endl;
    gossip->notify("女帝汉库克想要嫁给路飞, 给路飞生猴子, 哈哈哈...");

    delete ms;
    delete gossip;
    delete dragon;
    delete shanks;
    delete barto;

    return 0;
}

策略模式(抽象策略类来完成策略选择而不是在对象本身做这件事儿)

策略模式需要我们定义一系列的算法,并且将每种算法都放入到独立的类中,在实际操作的时候使这些算法对象可以相互替换。

出行策略,可以选择不同的交通工具:自行车、公交、地铁、自驾等

战国时期秦国的外交政策:远交近攻

收复台湾的策略:武统、文统、恩威并施 。。。

电商平台的打折策略:买二赠一、满300减50、购买VIP享8折优惠。。。

策略模式中的若干个策略对象相互之间是完全独立的, 它们不知道其他对象的存在。当我们想使用对象中各种不同的算法变体,并希望能够在运行的时候切换这些算法时,可以选择使用策略模式来处理这个问题。

状态模式(复杂的策略模式——可根据自身去调整策略)

状态模式就是在一个类的内部会有多种状态的变化,因为状态变化从而导致其行为的改变,在类的外部看上去这个类就像是自身发生了改变一样。

人在幼年、童年、少年、中年、老年各个使其的形态都是不一样的

工作期间,上午、中午、下午、傍晚、深夜的工作状态也不一样

人的心情不同时,会有喜、怒、哀、乐

手机在待机、通话、充电、游戏时的状态也不一样

文章的发表会有草稿、审阅、发布状态

状态模式和策略模式的比较

状态模式和策略模式比较类似,
策略模式中的各个策略是独立的不关联的,
但是状态模式下的对象的各种状态可以是独立的也可以是相互依赖的

普通用户的文章草稿发表之后被审阅,审阅失败重新变成草稿
管理用户的文章操作发布成功变成已发表状态, 发布失败重新变成草稿

实现方法

  • 给这些状态定义一个基类,通过提供的工作函数virtual working(*所有者)=0的参数指定了这个状态的所有者
  • 状态1: public 状态{void working(*所有者) override;}
  • 状态2: public 状态{void working(*所有者) override;}
  • 状态n: public 状态{void working(*所有者) override;}
  • 初始化实例(获得状态,传入状态调用work函数)

状态模式下各个模式之间是可以有依赖关系的,这一点和策略模式是有区别的,策略模式下各个策略都是独立的,当前策略不知道有其它策略的存在。

  • 如果对象需要根据当前自身状态进行不同的行为,
    同时状态的数量非常多且与状态相关的代码会频繁变更或者类对象在改变自身行为时需要使用大量的条件语句时,可使用状态模式。

模板方法模式(开发时发现不变系统的常用的行为,就抽象为模板方法)

模板方法模式就是在基类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。
说的再直白一些就是先定义一个基类,在基类中把与需求相关的所有操作函数全部作为虚函数定义出来,然后在这个基类的各个子类中重写父类的虚函数,这样子类基于父类的架构使自己有了和其他兄弟类不一样的行为。

盖房子:地基、建造架构相同,但是水电安装却可以不一样

造车、造船:使用相同的车、船架构可以造出很多不同型号的车、船

考试卷:试题相同,但是每个人书写的答案却不尽相同。

抽象类中提供了一些纯虚函数,这些纯虚函数在子类中是必须要进行重写的,否则子类也是抽象类,这样子类就无法实例化了。另外,还有一些虚函数,这些虚函数可以根据实际需求可以在子类中重写,也可以不重写。

(没有重写父类的这些虚函数,也就是意味着这些功能还没有实现)

不管是第一代还是第二代他们是发生了改变,
但这些变化背后对应的却是不变,
那就是父类提供的架构没有改变。

我们在实现子类的时候,如果发现不变的行为和可变的行为混合在了一起,导致不变的行为在多个子类中重复出现,此时就可以使用模板方法模式把不变的行为搬到基类中,去除子类里边的重复代码,来体现它的优势,模板方法模式就是提供了一个很好的代码复用平台。

生成器模式和模板方法模式的异同

  • 生成器模式:

就像造房子,有一套流程(打地基、砌墙、盖屋顶),但可以用不同材料、不同风格,最后造出不同的房子。

  • 模板方法模式:

就像做蛋糕,流程(和面、烘烤、装饰)是固定的,但每一步可以用不同的方法(碱水or普通等等),最后都是蛋糕,只口感或者营养程度不同。

在这里插入图片描述

在这里插入图片描述

访问者模式(对象+算法,使用算法–>对象)

  • 先看几个例子

旅游:去安徽可以爬黄山,去山东可以爬泰山,去陕西可以爬华山

不同的地点,人爬的山是不一样的 卖保险:如果是老百姓推销医疗保险,如果是银行推销失窃保险,如果是商铺推销火灾和水灾保险。

不同的受众,保险推销员推销的产品是不一样的
小鬼子奇葩的盖章文化:Boss的章是正的,其他下属职位越低盖章的时候就得越倾斜(真他妈的虚伪),如果不这样就表示对上司有意见。

不同等级的职员,盖章的方式是不一样的

前者是对象,后者就是算法,如果用访问者模式处理上边列举的场景就需要使用后者来访问前者。

访问者模式的优点

访问者模式适用于数据结构比较稳定的系统,
一个例子就是草帽团成员:只有男性和女性(不会再出现其它性别)。
在剥离出的行为状态类中针对男性和女性提供了相对应的 doing 方法。
这种模式的优势就是可以方便的给对象添加新的状态和处理动作,也就是添加新的 AbstractAction 子类(算法类),在需要的时候让这个子类去访问某个成员对象,
访问者模式的最大优势就是使算法的增加变得更加容易维护。

如果不按照性别进行划分,草帽团一共9个成员就需要在行为状态类中给每个成员提供一个 doing 方法,当草帽团又添加了新的成员,状态类中也需要给新成员再添加一个对应的 doing 方法,
这就破坏了设计模式的开放 – 封闭原则。

  • 最后提醒一点

访问者不是常用的设计模式, 因为它不仅复杂, 应用范围也比较狭窄。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐