设计模式看这篇就够了

设计模式的笔记学习。

设计模式与原则

设计模式的主流开发原则

  • 单一职责原则
  • 开闭原则
  • 里氏替换原则
  • 依赖导致原则
  • 接口隔离原则
  • 迪米特法则
  • 合成/聚合复用原则

主流的设计模式有23种,主要介绍以下几种,其他方法等遇到了再学习:

  • 创造型模式:用来描述 “如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。包括单例、原型、工厂方法、抽象工厂和建造者 5 种模式。
    • 单例模式
    • 工厂方法模式
    • 建造者模式
  • 结构型模式:用来描述如何将多个类或对象按照某种布局组合成更大的结构。包括代理、适配器、桥接、装饰、外观、享元和组合 7 种模式。
    • 代理模式
    • 外观模式
    • 迭代器模式
    • 组合模式
    • 适配器模式
  • 行为型模式:用来识别多个类之间的常用交流模式以及如何分配职责
    • 策略模式
    • 模版方法模式
    • 观察者模式
      请添加图片描述

1. 设计原则介绍

1.1 依赖倒置原则

这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。也就是抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

1.2 开闭原则

一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

1.3 单一职责原则

一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。

1.4 里氏代换原则

在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

遵循这个原则的子类才有资格说继承自父类

1.5 接口隔离原则

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。简单的说就是接口应该小而完备。

根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。

1.6 合成复用原则

尽量使用对象组合(class A中包含classB),而不是继承来达到复用的目的。简单的说就是少用继承,而多用对象组合

合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承

1.7 迪米特原则

一个软件实体应当尽可能少地与其他实体发生相互作用

如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。

2. 设计模式的介绍(重点)

一个好的面向对象的设计是应对变化,提高复用

设计模式的关键是寻找变化点,并在变化点应用设计模式。所以一个需要应用设计模式的软件体系,需要既有稳定的地方,又有变化的地方。

“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。

重构关键的技法:

  • 静态->动态
  • 早绑定 -> 晚绑定
  • 继承 -> 组合
  • 编译时依赖 -> 运行时依赖
  • 紧耦合 -> 松耦合

一句话总结每个设计模式

  • 模板方法模式:父类定义并实现了动作及动作的执行动作(如AC,run( ) ), 保留了某个动作作为虚函数让子类定义(如B),不同子类重写B,再通过创建父类指针的子类对象,子类对象执行run() ,从而使其实现不同的动作(运用多态)。
  • 策略模式:存在三个类:环境类(用于使用策略类),抽象策略类(父类),具体策略类(继承父类的子类)。环境类通过 SetStrategy函数 用父类指针保存子类对象,再通过UseStrategy函数从而实现不同的策略(运用多态)
  • 工厂方法模式:一共有四个类:抽象产品类,具体产品类(继承抽象产品类),工厂基类,具体工厂类(继承工厂基类),具体工厂类和具体产品类一一对应。工厂类通过CreateProduct函数生产产品。具体工厂类通过继承工厂基类来返回具体的产品(返回值是抽象产品类的指针)。
  • 抽象工厂模式:与工厂方法模式类似,只是工厂可以产生一簇具有相关性的产品。
  • 单例模式:有些特殊的类,由于某种原因,规定他只能生成一个对象。单例模式就是解决一个类只能生成一个对象而产生的。
  • 代理模式:有些类,由于某种原因,无法直接访问,这时就需要一个代理类来实现对原类的间接访问。

注意,基类必须给出一个虚析构函数。(原因看C++那篇)

3. 组件协作模式(行为型模式)

“组件协作”模式通过晚期绑定,来实现框架与应用程序之 间的松耦合,是二者之间协作时常用的模式。

  • 模板方法模式
  • 策略模式
  • 观察者模式

3.1 模版方法模式

在现实生活中,很多事情都包含几个实现步骤,例如请客吃饭,无论吃什么,一般都包含点单、吃东西、买单等几个步骤,通常情况下这几个步骤的次序是:点单 --> 吃东西 --> 买单。在这三个步骤中,点单和买单大同小异,最大的区别在于第二步——吃什么?吃面条和吃满汉全席可大不相同,如图1所示:
请添加图片描述

在软件开发中,有时也会遇到类似的情况,某个方法的实现需要多个步骤(类似“请客”),其中有些步骤是固定的(类似“点单”和“买单”),而有些步骤并不固定,存在可变性(类似“吃东西”)。为了提高代码的复用性和系统的灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计,在模板方法模式中,将实现功能的每一个步骤所对应的方法称为基本方法(例如“点单”、“吃东西”和“买单”),而调用这些基本方法同时定义基本方法的执行次序的方法称为模板方法(例如“请客”)。在模板方法模式中,可以将相同的代码放在父类中,例如将模板方法“请客”以及基本方法“点单”和“买单”的实现放在父类中,而对于基本方法“吃东西”,在父类中只做一个声明,将其具体实现放在不同的子类中,在一个子类中提供“吃面条”的实现,而另一个子类提供“吃满汉全席”的实现 。通过使用模板方法模式,一方面提高了代码的复用性,另一方面还可以利用面向对象的多态性,在运行时选择一种具体子类,实现完整的“请客”方法,提高系统的灵活性和可扩展性。

模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

模板方法模式定义如下:
  定义一个操作中的算法的结构(稳定的,骨架也就是执行顺序),而将一些步骤延迟 (变化)到子类中。Template Method使得子类可以不改变 (复用)一个算法的结构即可重写该算法的 某些特定步骤。

个人总结:简单来说就是父类定义ABC三个动作;其中A、C动作由于大家都一样,所以父类直接实现,而B动作由于不同的业务会导致动作的不同,所以由子类自己实现的(所以B只需要在父类中定义成虚函数,父类不需要实现),然后在父类中需要在定义一个run方法,用来定义动作的执行次序。子类只需要实现父类的虚函数即可。

在这里插入图片描述
如上图所示,红色的部分是稳定的部分,蓝色的部分是变化的部分(会随着业务的不同而改变),框架开发人员会把程序的主流程也设计进框架内(父类),应用开发人员就不需要考虑设计程序主流程了,只需要开发步骤2、4。

/* 模板方法模式 */
class father
{
public:
    void A(){//稳定
        ...;
    }
    
    virtual void B()=0;//支持变化的。虚函数,由子类实现
    
    void C(){//稳定
        ...;
    }
    void run()//稳定
    {
        A();
        B();
        C();
    }
    
};

从上面的代码可以看出,run方法是稳定的,但是稳定中又带有变化(B方法支持重写)。

模版方法的缺陷:
  需要假定算法的结构(也就是run方法)是稳定的,也就是说所有子类的执行步骤都是一样的,才能使用模板方法的设计模式。

在看懂了前面对模板方法的介绍后,再来看下面的要点总结,不然你看不懂:

  • Template Method模式是一种非常基础性的设计模式,在面向对 象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性) 为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本 实现结构。
  • 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用 你”的反向控制结构是Template Method的典型应用。
  • 在具体实现方面,被Template Method调用的虚方法可以具有实 现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将 它们设置为protected方法。

3.1.1 案例

代码案例:

/* 模板方法模式案例
 * 假定一个场景:去餐馆吃饭的场景
 * 该场景包含了三个动作:点单 吃东西 买单
 * 点单和买单大同小异,最大的区别在于第二步——吃什么?
 * */
#include <iostream>
#include <string>
using namespace std;

class Abstract_eat
{
public:
    void Order()
    {
        cout<<"点菜!"<<endl;
    }
    void Pay()
    {
        cout<<"买单!"<<endl;
    }

    void Run()//稳定
    {
        Order();//稳定
        Eat();//变化,不同的业务这里体现不同
        Pay();//稳定
    }
    virtual ~Abstract_eat(){}

protected:
    virtual void Eat()=0;
};
/* 应用人员只需要重写虚函数就好了 */
class Tom_eat: public Abstract_eat
{
    void Eat()
    {
        cout<<"吃面!"<<endl;
    }
};

class Bob_eat: public Abstract_eat
{
    void Eat()
    {
        cout<<"吃麦当劳!"<<endl;
    }
};




int main() {

    Abstract_eat * Bob = new Bob_eat();
    cout<<"鲍勃的吃东西的执行流程:"<<endl;
    Bob->Run();

    cout<<endl;

    Abstract_eat * Tom = new Tom_eat();
    cout<<"汤姆的吃东西的执行流程:"<<endl;
    Tom->Run();

    return 0;
}
输出结果:

鲍勃的吃东西的执行流程:
点菜!
吃麦当劳!
买单!

汤姆的吃东西的执行流程:
点菜!
吃面!
买单!

3.2 策略模式

在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放 在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。

策略模式定义如下:
  定义一系列算法,把它们一个个封装起来,并且使它们可互 相替换(变化)。该模式使得算法可独立于使用它的客户程 序(稳定)而变化(扩展,通过子类化)。

在策略模式中包含如下几个角色:

  • Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
  • Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
  • ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

个人总结:简单的说就是在算法的实现依赖于策略,策略可以根据继承抽象父类选择不同的策略来实现。具体实现其实就是定义一个环境类(也就是使用不同方法的类),在定义一个抽象的父类(作为接口),具体实现的子类(也称具体策略类),重写抽象父类的虚函数(重写接口),环境类通过调用父类中的虚方法(实现多态)来调用不同的子类,从而实现灵活的选择不同的方法。

如果看懂了上面的介绍,下面的结构图应该就能看懂了(图中每个类的介绍看上面)
在这里插入图片描述

要点总结:

  • Strategy及其子类为组件提供了一系列可重用的算法,从而可以使 得类型在运行时方便地根据需要在各个算法之间进行切换。
  • Strategy模式提供了用条件判断语句以外的另一种选择,消除条件 判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需 要Strategy模式。
  • 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个 Strategy对象,从而节省对象开销。(单例模式具体再介绍)

策略模式的好处用扩展的方式面对未来需求的变化。只需要新写一个类,就能扩展一种方法。

策略模式缺陷:
如果你的环境类不会改变,也就是一定只会用abc三种算法,不会用到其他算法,那么就不需要用策略模式。

/*
 * 根据结构图抽象的实现
 */

//它为所支持的算法声明了抽象方法,是所有策略类的父类
class Strategy
{
public:
    virtual void algorithm()=0;//抽象接口
    virtual ~Strategy(){};
};

class Context//使用算法(策略)的角色,用于解决某个具体问题
{
private:
    //具体策略
    Strategy *  m_strategy;
public:
    void SetStrategy(Strategy * strategy)
    {
        //获取具体的方法(父类指针指向子类对象)
        this->m_strategy = strategy;
    }
    void UseStrategy()
    {
        m_strategy->algorithm();//多态
    }

};

/*
 * 具体策略类
 */
class Strategy_A:public Strategy
{
public:
    void algorithm()
    {
        cout<<"策略A"<<endl;
    }
};

class Strategy_B:public Strategy
{
public:
    void algorithm()
    {
        cout<<"策略B"<<endl;
    }
};

3.2.1 案例

/*
 * 策略模式案例
 *  假设一个场景:有税率计算问题,每个国家的税率的计算公式都不同
 * */

//抽象税率父类
class Strategy_Tex
{
public:
    virtual void Calculate()=0;//抽象接口
    virtual ~Strategy_Tex(){};
};

//税率的计算
class Context_SalesOrder
{
private:
    //用于保存具体某一国家计算税率的方法的对象
    Strategy_Tex *  m_strategy;
public:
    void SetStrategy(Strategy_Tex * strategy)
    {
        //获取某一国家的税率方法的对象(父类指针指向子类对象)
        this->m_strategy = strategy;
    }

    //具体使用某一国家的税率的方法
    void UseStrategy()
    {
        m_strategy->Calculate();//多态
    }

};

/*
 * 具体每个国家计算税率的方法:
 */
class CNTax:public Strategy_Tex
{
public:
    void Calculate()
    {
        cout<<"公式A计算中国税率"<<endl;
    }
};

class USTax:public Strategy_Tex
{
public:
    void Calculate()
    {
        cout<<"公式B计算中国税率"<<endl;
    }
};

//新写一个类,就能扩展一种计算某个国家的税率
class DETax:public Strategy_Tex
{
public:
    void Calculate()
    {
        cout<<"公式C计算中国税率"<<endl;
    }
};

int main()
{
    Context_SalesOrder c = Context_SalesOrder();

    CNTax * CNtax = new CNTax;
    USTax * UStax = new USTax;

    c.SetStrategy(CNtax);
    c.UseStrategy();

    c.SetStrategy(UStax);
    c.UseStrategy();
}
输出结果:

公式A计算中国税率
公式B计算中国税率

3.3 观察者模式

观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

定义:
  定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式。

结构图:
在这里插入图片描述
在观察者模式结构图中包含如下几个角色:

  • Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
  • ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
  • Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
  • ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除

要点总结:

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

注意:一般不推荐使用多继承,使用多继承的唯一用于是:一个作为主的继承实现,其他继承作为接口实现。

3.3.1 案例

/* 观察者模式案例
 *  假定一个场景:有个任务会根据自身状态,会自动通知其他对象(订阅者/观察者)做出反应
 *  任务就是目标,其他对象接收任务的通知就是观察者
 * */
#include <iostream>
#include <unistd.h>
#include <string>
#include <list>
using namespace std;

//抽象观察者类
class Observer
{
public:
    virtual void Update(int value)=0;
    virtual ~Observer(){};
};

//具体观察者类
class aObserver: public Observer
{
    void Update(int value)
    {
        cout<<"A观察者:已观察到目标改变状态为"<<value<<","<<"自身做出改变:"<<value*36<<endl;
    }
};

class bObserver: public Observer
{
    void Update(int value)
    {
        cout<<"B观察者:已观察到目标改变状态为"<<value<<","<<"自身做出改变:"<<value*10<<endl;
    }
};

//目标类
class Subject
{
private:
    list<Observer *> m_ObserverList;//抽象通知机制
public:
    void runTask()
    {
        for(int i=0; i<5; ++i)//模拟任务
        {
            //通知观察者状态改变
            Notify(i);
            sleep(2);
        }
    }

    //增加观察者
    void Attach(Observer * observer)
    {
        m_ObserverList.push_back(observer);
    }

    //删除观察者
    void Detach(Observer * observer)
    {
        m_ObserverList.remove(observer);
    }

    //该函数用于通知所有观察者
    void Notify(int value)
    {
        for(auto & ob :m_ObserverList)
        {
            ob->Update(value);
        }
    }
};

int main() {

    //实现观察者对象
    aObserver aobserver;
    bObserver bobserver;

    //实现目标对象
    Subject subject;

    //添加订阅
    subject.Attach(&aobserver);
    subject.Attach(&bobserver);

    //目标执行任务
    subject.runTask();
    
    return 0;
}
输出结果:

A观察者:已观察到目标改变状态为0,自身做出改变:0
B观察者:已观察到目标改变状态为0,自身做出改变:0
A观察者:已观察到目标改变状态为1,自身做出改变:36
B观察者:已观察到目标改变状态为1,自身做出改变:10
A观察者:已观察到目标改变状态为2,自身做出改变:72
B观察者:已观察到目标改变状态为2,自身做出改变:20
A观察者:已观察到目标改变状态为3,自身做出改变:108
B观察者:已观察到目标改变状态为3,自身做出改变:30
A观察者:已观察到目标改变状态为4,自身做出改变:144
B观察者:已观察到目标改变状态为4,自身做出改变:40
\

4. 对象创建模式(创造型模式)

通过“对象创建” 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。

4.1 工厂方法模式

工厂模式的前身还有个简单工厂,但是违背了开闭原则,所以不使用。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

工厂方法模式定义如下:
  定义一个用于创建对象的接口,让子类决定实例化哪一个类。 Factory Method使得一个类的实例化延迟(目的:解耦, 手段:虚函数)到子类。

个人总结:简单来说就是不用new了,而是由子类工厂对象通过继承工厂基类的创造产品的函数,来决定是实例化哪个具体产品。具体来说有四个类,抽象工厂基类的创造产品函数返回值是抽象产品,而子类工厂返回具体的产品(具体的产品继承了抽象产品)。

工厂方法模式结构
在这里插入图片描述

在工厂方法模式结构图中包含如下几个角色:

  • Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
  • ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应
  • Factory(工厂基类):在工厂基类中,声明了工厂方法(Factory Method),返回一个抽象产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
  • ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

工厂基类声明createproduct函数,返回值为抽象产品,具体工厂继承工厂基类,返回具体的具体产品。

要点总结:

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

缺点在于要求创建方法/参数相同

4.1.1 案例

/* 工厂方法模式案例
 *  假定一个场景:有个业务场景会需要使用不同的文件分割器来分割文件,文件分割器有很多种类型,比如图片分割,视频分割,TxT分割。
 *  根据不同的业务需要,需要使用不同的分割器来分割。
 *  因此需要工厂方法模式:按照业务的需要进行产生不同的分割器对象来完成业务需要
 * */
#include <iostream>
#include <string>
using namespace std;

/* 抽象产品类 */
class Product_ISplitter
{
public:
    virtual void split() = 0;
    virtual ~Product_ISplitter(){};
};

/* 具体产品类 */
class Product_PictureSplitter:public Product_ISplitter
{
public:
    virtual void split()
    {
        cout<<"图片分割"<<endl;
    }
};

class Product_VideoSplitter:public Product_ISplitter
{
public:
    virtual void split()
    {
        cout<<"视频分割"<<endl;
    }
};

class Product_TxtSplitter:public Product_ISplitter
{
public:
    virtual void split()
    {
        cout<<"Txt分割"<<endl;
    }
};

/* 工厂基类 */
class Factory
{
public:
    virtual Product_ISplitter * CreateProduct()=0;
    virtual ~Factory(){}

};

/* 具体工厂类--与具体产品类对应 */

//生产图片分割器类的工厂
class ConcreteFactory_PictureSplitter:public Factory
{
public:
    //注意这里返回的对象是抽象产品类
    virtual Product_ISplitter * CreateProduct()
    {
        return new Product_PictureSplitter();
    }
};

//生产视频分割器类的工厂
class ConcreteFactory_VideoSplitter:public Factory
{
public:
    //注意这里返回的对象是抽象产品类
    virtual Product_ISplitter * CreateProduct()
    {
        return new Product_VideoSplitter();
    }
};

//生产视频分割器类的工厂
class ConcreteFactory_TxtSplitter:public Factory
{
public:
    //注意这里返回的对象是抽象产品类
    virtual Product_ISplitter * CreateProduct()
    {
        return new Product_TxtSplitter();
    }
};

int main() {
    //使用工厂方法模式生产不同的分割器

    Factory * factory = new ConcreteFactory_VideoSplitter();//抽象工厂类的指针 指向 具体工厂类

    //这里会根据上一条代码new的不同的具体工厂而产生不同的产品(利用了多态)。注意:这里也是用抽象产品类来接收具体产品类
    Product_ISplitter * product = factory->CreateProduct();
    
    product->split();//利用了产品类的多态的特性,执行不同产品的功能(具体产品类重写了抽象产品类的split函数)


    return 0;
}
输出结果:

图片分割
视频分割

4.2 抽象工厂模式

抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品

抽象工厂模式定义如下:
  提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

个人总结:其实和工厂方法模式很像,工厂方法模式是抽象工厂模式的一个特例。具体区别在于:抽象工厂模式会生产多个有相互依赖关系的对象作为一簇产品。而工厂方法只会生产一个对象作为一个产品

在这里插入图片描述

在抽象工厂模式结构图中包含如下几个角色:

  • AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
  • ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
  • AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
  • ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

要点总结:

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

4.2.1 案例

/* 工厂方法模式案例
 *  假定一个场景:有个业务场景可能会使用不同的数据库在处理数据。并且每个数据库都有三个动作:连接,sql命令,读数据
 *  根据不同的业务需要,需要使用不同的数据库
 *  因此需要工厂方法模式:按照业务的需要进行产生不同的数据库来完成业务需要
 * */
#include <iostream>
#include <string>
using namespace std;

/* 抽象产品类(数据库访问有关的基类) */
class DBConnect
{
public:
    virtual void Connect() = 0;
    virtual ~DBConnect(){};
};

class DBCommand
{
public:
    virtual void Command() = 0;
    virtual ~DBCommand(){};
};

class DBReader
{
public:
    virtual void Reader() = 0;
    virtual ~DBReader(){};
};

/* 具体产品类 */

//mysql相关
class MysqlConnect:public DBConnect
{
public:
    virtual void Connect()
    {
        cout<<"mysql连接!"<<endl;
    }
};

class MysqlCommand:public DBCommand
{
public:
    virtual void Command()
    {
        cout<<"mysql命令!"<<endl;
    }
};

class MysqlReader:public DBReader
{
public:
    virtual void Reader()
    {
        cout<<"mysql数据读取!"<<endl;
    }
};

//oracle相关
class oracleConnect:public DBConnect
{
public:
    virtual void Connect()
    {
        cout<<"oracle连接!"<<endl;
    }
};

class oracleCommand:public DBCommand
{
public:
    virtual void Command()
    {
        cout<<"oracle命令!"<<endl;
    }
};

class oracleReader:public DBReader
{
public:
    virtual void Reader()
    {
        cout<<"oracle数据读取!"<<endl;
    }
};

/* 工厂基类 */
class Factory
{
public:
    virtual DBConnect * CreateDBConnect()=0;
    virtual DBCommand * CreateDBCommand()=0;
    virtual DBReader* CreateReader()=0;
    virtual ~Factory(){}

};

/* 具体工厂类--与具体产品类对应 */

//Mysql相关的工厂
class MysqlFactory:public Factory
{
public:
    //注意这里返回的对象是抽象产品类
    virtual DBConnect * CreateDBConnect()
    {
        return new MysqlConnect();
    }
    virtual DBCommand * CreateDBCommand()
    {
        return new MysqlCommand();
    }
    virtual DBReader* CreateReader()
    {
        return new MysqlReader();
    }
};

//Mysql相关的工厂
class oracleFactory:public Factory
{
public:
    //注意这里返回的对象是抽象产品类
    virtual DBConnect * CreateDBConnect()
    {
        return new oracleConnect();
    }
    virtual DBCommand * CreateDBCommand()
    {
        return new oracleCommand();
    }
    virtual DBReader* CreateReader()
    {
        return new oracleReader();
    }
};

int main() {
    //使用抽象工厂模式来生产不同数据库以满足业务需要

    Factory * factory = new oracleFactory();
    DBConnect * dbconnect = factory->CreateDBConnect();
    DBCommand * dbcommand = factory->CreateDBCommand();
    DBReader * dbreader = factory->CreateReader();
    dbconnect->Connect();
    dbcommand->Command();
    dbreader->Reader();

    cout<<endl;

    factory = new MysqlFactory();
    dbconnect = factory->CreateDBConnect();
    dbcommand = factory->CreateDBCommand();
    dbreader = factory->CreateReader();
    dbconnect->Connect();
    dbcommand->Command();
    dbreader->Reader();

}
输出结果

oracle连接!
oracle命令!
oracle数据读取!

mysql连接!
mysql命令!
mysql数据读取!

4.3 单例模式

前面的工厂模式绕过new是解决紧耦合问题,而单例模式绕过new是业务需要或者是性能的原因

动机:

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

单例模式分为懒汉模式和饿汉模式:

  • 懒汉模式:非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化。
  • 饿汉模式:即迫不及待,在程序运行时立即初始化(比较简单)。

实现方法:
  构造函数和拷贝构造函数都设置成private,并设置getInstance方法获取对象。

单例模式定义如下:
  确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

4.3.1 案例

/*
 * 单例模式案例
 * */

#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;

class Single1
{
public:
    static Single1 * GetInstance();
private:
    static Single1 * m_instance;
    Single1(){};
    Single1(const Single1 & s){};
};

/* 饿汉模式:即迫不及待,在程序运行时立即初始化(比较简单)*/
Single1 * Single1::m_instance = new Single1();
Single1 * Single1::GetInstance()
{
    return m_instance;
}

/* 懒汉模式:非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化。有三种初始化方法*/

class Single2
{
public:
    static Single2 * GetInstance();
private:
    static pthread_mutex_t m_lock;
    static Single2 * m_instance;
    Single2(){};
    Single2(const Single2 & s){};
};

//1、线程非安全版本
Single2  * Single2::GetInstance()
{
    if(m_instance == nullptr)
    {
        m_instance = new Single2();
    }
    return m_instance;
}

//2、线程安全版本,但是锁的代价过高
Single2 * Single2::GetInstance()
{
    pthread_mutex_lock(&m_lock);
    if(m_instance == nullptr)
    {
        m_instance = new Single2();
    }
    pthread_mutex_unlock(&m_lock);
    return m_instance;
}

//3、双检查锁,但由于内存读写reorder不安全
Single2 * Single2::GetInstance()
{
    if(m_instance == nullptr)
    {
        pthread_mutex_lock(&m_lock);
        if(m_instance == nullptr)
        {
            m_instance = new Single2();
        }
        pthread_mutex_unlock(&m_lock);
    }
    return m_instance;
}


int main()
{
    Single2 * a = Single2::GetInstance();
}

注意:在案例中使用了双检查锁

为什么要用双检测,只检测一次不行吗?

如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

并且,双检查锁会有reorder现象:编译器在正式编译时,不一定是按照先给内存分配对象,再将内存返回给m_instance,而有可能会先给m_instance分配一个空的内存,再将对象分配给m_instance,而在给他分配空的内存的时候,第二个线程来了,此时就会得到一个空内存的m_instance,会出错。

5. 结构型模式

5.1 代理模式

动机:
  在面向对象系统中,有些对象由于某种原因(比如对象创建的开销很大,或者某些操作需要安全控制,或者需要进程外的访问等)无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问

代理模式定义如下:
  给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

核心思想:增加一个间接层,来实现一些功能(性能优化、无法直接访问)。

在真正业务处理时:proxy的设计一般会比较复杂。

要点总结:

  • 增加一层间接层”是软件系统中对许多复杂问题的一种常见解决方
    法。在面向对象系统中,直接使用某些对象会带来很多问题,作为
    间接层的proxy对象便是解决这一问题的常用手段。
  • 具体proxy设计模式的实现方法、实现粒度都相差很大,有些可能
    对单个对象做细粒度的控制,如copy-on-write技术,有些可能对组
    件模块提供抽象代理层,在架构层次对对象做proxy。
  • Proxy并不一定要求保持接口完整的一致性,只要能够实现间接控制,有时候损及一些透明性是可以接受的。

5.2 外观(门面)模式

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

不知道大家有没有比较过自己泡茶和去茶馆喝茶的区别,如果是自己泡茶需要自行准备茶叶、茶具和开水,而去茶馆喝茶,最简单的方式就是跟茶馆服务员说想要一杯什么样的茶,是铁观音、碧螺春还是西湖龙井?正因为茶馆有服务员,顾客无须直接和茶叶、茶具、开水等交互,整个泡茶过程由服务员来完成,顾客只需与服务员交互即可,整个过程非常简单省事。

因此,在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,由它来负责和多个业务类进行交互,而客户类只需与该类交互。 外观模式通过引入一个新的外观类(Facade)来实现该功能,外观类充当了软件系统中的“服务员”,它为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互。在外观模式中,那些需要交互的业务类被称为子系统(Subsystem)。如果没有外观类,那么每个客户类需要和多个子系统之间进行复杂的交互,系统的耦合度将很大,如下图(A)所示;而引入外观类之后,客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂引用关系由外观类来实现,从而降低了系统的耦合度,如下图(B)所示:

请添加图片描述

外观模式定义如下:
  为子系统中的一组接口提供一个统一(稳定)的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用(复用)。
在这里插入图片描述

个人总结: 一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开(解耦合),使得客户类只需要与外观类打交道就可以实现与内部进行通信,而不需要与子系统内部的很多对象打交道。

要点总结:

  • 从客户程序的角度来看,Facade模式简化了整个组件系统的接口,对于组件内部与外部客户程序来说,达到了一种**“解耦”**的效果,内部子系统的任何变化不会影响到Fagade 接口的变化。
  • Facade设计模式更注重从架构的层次去看整个系统,而不是单个类的层次。Facade很多时候更是一种架构设计模式。
  • Facade设计模式并非一个集装箱,可以任意地放进任何多个对象。Fagade模式中组件的内部应该是“ 相互耦合关系比较大的系列组件”,而不是一个简单的功能集合。

5.3 迭代器模式

动机:
在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明地访问其中包含的元素;同时这种“透明遍历”也为"同一种算法在多种集合对象上进行操作”提供了可能。

在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。

定义:
  提供一种方法来访问聚合对象,而不用暴露(稳定)这个对象的内部表示。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值