设计模式(全23种)

1.前言

1.C++UML类图

面向对象设计主要就是使用UML的类图,类图用于描述系统中所包含的类以及它们之间的相互关系,帮助人们简化对系统的理解,它是系统分析和设计阶段的重要产物,也是系统编码和测试的重要模型依据。下面基于C++这门语言给大家讲一下UML类图的画法。

类的UML画法

类(class / struct)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性操作关系的对象集合的总称。在系统中,每个类都具有一定的职责,职责指的是类要完成什么样子的功能,要承担什么样子的义务。一个类可以有多种职责,但是设计得好的类一般只有一种职责。

比如,我现在定义了猎人类:

C++
class Hunter
{
public:
    int m_age = 32;
    static int m_times;
    string getName()
    {
        return m_name;
    }

    void setName(string name)
    {
        m_name = name;
    }

    void goHunting()
    {
        aiming();
        shoot();
    }
    static void saySorry()
    {
        string count = to_string(m_times);
        cout << "Say sorry to every animal " + count + " times!" << endl;
    }

protected:
    string m_name = "Jack";
    void aiming()
    {
        cout << "使用" + m_gunName + "瞄准猎物..." << endl;
    }

private:
    string m_gunName = "AK-47";
    void shoot()
    {
        cout << "使用" + m_gunName + "射击猎物..." << endl;
    }
};
int Hunter::m_times = 3;

上面这个类对应的类图应该是这样的:

可以看到该图分为上中下三部分:上层是类名,中间层是属性(类的成员变量),下层是方法(类的成员函数)。

  • 可见性:+ 表示public# 表示protected- 表示private__(下划线)表示static
  • 属性的表示方式:【可见性】【属性名称】:【类型】= { 缺省值,可选 }
  • 方法的表示方式:【可见性】【方法名称】(【参数名 : 参数类型,......】):【返回值类型】

如果我们定义的类是一个抽象类(类中有纯虚函数),在画UML类图的时候,类名需要使用斜体显示

在使用UML画类图的时候,虚函数的表示方跟随类名,也就是使用斜体,如果是纯虚函数则需要在最后给函数指定=0

类与类之间的关系

继承关系

继承也叫作泛化(Generalization),用于描述父子类之间的关系,父类又称为基类或者超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的实线来表示。

关于继承关系一共有两种:普通继承关系抽象继承关系,但是不论哪一种表示继承关系的线的样式是不变的。

假如现在我定义了一个父类(Bird)和两个子类(CuckooEagle):

C++

class Bird
{
public:
    string getName()
    {
        return m_name;
    }

    void setName(string name)
    {
        m_name = name;
    }

    virtual void fly() {}
    virtual void eat() {}
protected:
    string m_sex;
    string m_name;
};

class Cuckoo : public Bird
{
public:
    void fly() override
    {
        cout << "我拍打翅膀飞行..." << endl;
    }

    void eat() override
    {
        cout << "我喜欢吃肉肉的小虫子..." << endl;
    }
};

class Eagle : public Bird
{
public:
    void fly() override
    {
        cout << "我展翅翱翔..." << endl;
    }

    void eat() override
    {
        cout << "我喜欢吃小动物..." << endl;
    }
};

使用UML表示上述这种关系应当是:

父类Bird中的fly()eat()是虚函数,它有两个子类CuckooEagle在这两个子类中重写了父类的虚函数,在使用带空心三角的实现表示继承关系的时候,有空心三角的一端指向父类,另一端连接子类

关联关系

关联(Assocition)关系是类与类之间最常见的一种关系,它是一种结构化的关系,表示一个对象与另一个对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等。在UML类图中,用(带接头或不带箭头的)实线连接有关联关系的类。在C++中这种关联关系在类中是这样体现的,通常将一个类的对象作为另一个类的成员变量

类之间的关联关系有三种,分别是:单向关联双向关联自关联。下面逐一给大家进行介绍。

单向关联关系

单向关联指的是关联只有一个方向,比如每个孩子(Child)都拥有一个父亲(Parent),其代码实现为:

C++
class Parent
{
};

class Child
{
private:
    Parent m_father;
};

通过UML来说描述这两个类之间的关系,如下图:

如果是单向关联,使用的连接线是带单向箭头的实线, 哪个类作为了当前类的成员变量,那么箭头就指向哪个类。在这个例子中 Parent 类 作为了Child 类的成员变量,因此箭头端应该指向Parent 类,另一端连接 Child 类

双向关联关系

现实生活中每个孩子都有父母,每个父母同样有自己的孩子,如果想要通过类来描述这样的亲情关系,代码如下:

C++
class Parent
{
private:
    Child m_son;
};

class Child
{
private:
    Parent m_father;
};

通过UML来说描述这两个类之间的关系,如下图:

在画UML类图的时候,一般使用没有箭头的实线来连接有双向关联关系的两个类,这两个类的对象分别作为了对方类的成员变量。

有些UML绘图软件使用的是带双向箭头的实线来表示双向关联关系。

自关联关系

自关联指的就是当前类中包含一个自身类型的对象成员,这在链表中非常常见,单向链表中都会有一个指向自身节点类型的后继指针成员,而双向链表中会包含一个指向自身节点类型的前驱指针和一个指向自身节点类型的后继指针。就以双向链表节点类为例,它的C++写法为:

C++
class Node 
{
private:
    void* m_data;
    Node* m_prev;
    Node* m_next;
};

对应的UML类图应当是:

一般使用带箭头的实线来描述自关联关系,我中有我,独角戏。

有些UML绘图软件表示类与类的关联关系,使用的就是一条实线,没有箭头。

聚合关系

聚合(Aggregation)关系表示整体部分 的关系。在聚合关系中,成员对象是整体的一部分,但是成员对象可以脱离整体对象独立存在 。在UML中,聚合关系用带空心菱形的直线表示,下面举两个聚合关系的例子:

  • 汽车(Car)与 引擎(Engine)、轮胎(Wheel)、车灯(Light)
  • 森林(Forest)与 植物(Plant)、动物(Animal)、水(Water)、阳光(Sunshine)

以森林为例,对应的C++类的定义如下:

C++
class Plant
{
    // 植物
};

class Animal
{
    // 动物
};

class Water
{
    // 水
};

class Sunshine
{
    // 阳光
};

class Forest
{
public:
    Forest(Plant p, Animal a, Water w, Sunshine s) : 
        m_plant(p),m_animal(a),m_water(w),m_sun(s)
    {
    }
private:
    Plant m_plant;
    Animal m_animal;
    Water m_water;
    Sunshine m_sun;
};

对应的UML类图为:

代码实现聚合关系,成员对象通常以构造方法、Setter方法的方式注入到整体对象之中,因为成员对象可以脱离整体对象独立存在。

表示聚合关系的线,有空心菱形的一端指向整体对象,另一端连接局部对象(有些UML绘图软件在这一端还带一个箭头)。

组合关系

组合(Composition)关系也表示的是一种整体和部分的关系 ,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也不存在,整体对象和成员对象之间具有同生共死的关系

在UML中组合关系用带实心菱形的直线表示,下面举两个组合关系的例子:

  • 头(Head)和 嘴巴(Mouth)、鼻子(Nose)、耳朵(Ear)、眼睛(Eye)
  • 树(Tree)和 树根(Root)、树干(Trunk)、树枝(Branch)、树叶(Leaf)

以树为例,对应的C++类的定义如下:

C++
class Root
{
};

class Trunk
{
};

class Branch
{
};

class Leaf
{
};

class Tree
{
public:
    Tree()
    {
        m_root = new Root;
        m_trunk = new Trunk;
        m_branch = new Branch;
        m_leaf = new Leaf;
    }
    ~Tree()
    {
        delete m_root;
        delete m_trunk;
        delete m_branch;
        delete m_leaf;
    }
private:
    Root* m_root;
    Trunk* m_trunk;
    Branch* m_branch;
    Leaf* m_leaf;
};

其UML的表示方法为:

代码实现组合关系,通常在整体类的构造方法中直接实例化成员类,因为组合关系的整体和部分是共生关系,整体的实例对象被析构的时候它的子对象也会一并被析构。如果通过外部注入,即使整体不存在了,部分还是存在的,这样的话就变成聚合关系了。

依赖关系

依赖(Dependency)关系是一种使用关系 ,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系,大多数情况下依赖关系体现在某个类的方法使用另一个类的对象作为参数。

在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方,下面举两个依赖关系的例子:

  • 驾驶员(Driver)开车,需要将车(Car)对象作为参数传递给 Driver 类的drive()方法。

    C++
    class Car 
    { 
    public: 
        void move() {}
    }; 
    
    class Driver 
    {
    public: 
        void drive(Car car) 
        { 
            car.move(); 
        } 
    };
    
  • 树木(Tree)的生长,需要将空气(Air)、水(Water)、土壤(Soil)对象作为参数传递给 Tree 类的 grow()方法。

    C++
    class Water
    {
    };
    
    class Air
    {
    };
    
    class Soil
    {
    };
    
    class Tree
    {
    public:
        void grow(Water w, Air a, Soil s) 
        {
            cout << "借助 w 中的水分, s 中的养分和 a 中的二氧化碳, 我就可以茁壮成长了";
        }
    };
    

关于树木这个类,它对应的UML类图为:

依赖关系通常通过三种方式来实现:

  1. 将一个类的对象作为另一个类中方法的参数

  2. 在一个类的方法中将另一个类的对象作为其对象的局部变量

  3. 在一个类的方法中调用另一个类的静态方法

    类之间的关系强弱顺序是这样的:继承(泛化) > 组合 > 聚合 > 关联 > 依赖。

关联关系、聚合关系、组合关系之间的区别

从上文可以看出,关联关系、聚合关系和组合关系三者之间比较相似,最后就来总结一下这三者之间的区别:

  1. 关联和聚合的区别主要在于语义上:关联的两个对象之间一般是平等的,聚合则一般是不平等的
  2. 聚合和组合的区别则在语义和实现上都有差别:
    • 组合的两个对象之间生命周期有很大的关联,被组合的对象在组合对象创建的同时或者创建之后创建在组合对象销毁之前销毁,聚合则无需考虑这些事情。
    • 一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象,聚合则不一样,被聚合的对象可以属于多个聚合对象。

最后,再举例子来描述一下这三种关系:

  1. 朋友之间属于关联关系,因为这种关系是平等的,关联关系只是用于表示两个对象之间的一种简单的联系而已。
  2. 图书馆看书的时候,人和书属于聚合关系。书是可以独立存在的,而且书不仅可以属于自己,也可以属于别人。
  3. 人和自己的心脏属于组合关系,因为心脏不能脱离人体而独自存在。

不过,实际应用中,这三种关系的界限划分其实没有那么清楚,有些时候我们会感觉组合和聚合没什么区别,所以,在设计的时候没必要死抠细节,只要能够利用对象之间的关系设计出可行的解决方案即可。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/UML-class-diagrams/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


2.设计模式三原则

我们在进行程序设计的时候,要尽可能地保证程序的可扩展性、可维护性和可读性,所以需要使用一些设计模式,这些设计模式都遵循了以下三个原则,下面来依次为大家介绍。

单一职责原则

C++面向对象三大特性之一的封装指的就是将单一事物抽象出来组合成一个类,所以我们在设计类的时候每个类中处理的是单一事物而不是某些事物的集合。

设计模式中所谓的单一职责原则,就是对一个类而言,应该仅有一个引起它变化的原因,其实就是将这个类所承担的职责单一化(就跟海贼王中的能力者一样,每个人只能吃一颗恶魔果实,拥有某一种能力【黑胡子这个Bug除外】)。

如果一个类承担的职责过多,就等于把这些职责耦合到了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致设计变得脆弱,当变化发生时,设计会遭受到意想不到的破坏。

偷袭白胡子的这个男人被白胡子视为自己的儿子,他叫斯库亚德,本来是一起去救艾斯的【此时的他是一个单一职责的类】,后来被赤犬挑拨离间想到了自己的过去,并萌生了别的想法【这个类被追加了一些其他的职责】,最终背叛并刺穿了白胡子的身体【这个类没能完成开始时的预期任务,就此废掉了】。由此可见,让一个类承担太多的职责绝非好事。

软件设计真正要做的事情就是,发现根据需求发现职责,并把这些职责进行分离,添加新的类,给当前类减负,越是这样项目才越容易维护。

开放封闭原则

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

该原则是程序设计的一种理想模式,在很多情况下无法做到完全的封闭。但是作为设计人员,应该能够对自己设计的模块在哪些位置产生何种变化了然于胸,因此需要在这些位置创建抽象类来隔离以后发生的这些同类变化(其实就是对多态的应用,创建新的子类并重写父类虚函数,用以更新处理动作)。

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

草帽团船长路飞从出海到现在一共召集了9个伙伴,这些伙伴在船上的职责都是不一样的,有音乐家船工舵手航海士剑士考古学家狙击手厨师船医,作为船长没有要求自己学习这些船员的技能【对自己来说是封闭的】,而是提出了伙伴的概念【这就是一个可变的抽象】,最终找到了优秀的伙伴加入【对外是开放的,每个伙伴都是这个抽象的具体实现,但他们的技能又有所不同】,事实证明这样做是对的,如果反其道而行之,不仅违背了开放封闭原则,也违背了单一职责原则。

开放 -- 封闭原则是面向对象设计的核心所在,这样可以给我们设计出的程序带来巨大的好处,使其可维护性、可扩展性、可复用性、灵活性更好。

依赖倒转原则

关于依赖倒转原则,对应的是两条非常抽象的描述:

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

先用人话解释一下这两句话中的一些抽象概念:

  • 高层模块:可以理解为上层应用,就是业务层的实现
  • 低层模块:可以理解为底层接口,比如封装好的API、动态库等
  • 抽象:指的就是抽象类或者接口,在C++中没有接口,只有抽象类

先举一个高层模块依赖低层模块的例子:

大聪明的项目组接了一个新项目,低层使用的是MySql的数据库接口,高层基于这套接口对数据库表进行了添删查改,实现了对业务层数据的处理。而后由于某些原因,要存储到数据库的数据量暴增,所以更换了Oracle数据库,由于低层的数据库接口变了,高层代码的数据库操作部分是直接调用了低层的接口,因此也需要进行对应的修改,无法实现对高层代码的直接复用,大聪明欲哭无泪。

通过上面的例子可以得知,当依赖的低层模块变了就会牵一发而动全身,如果这样设计项目架构,对于程序猿来说,其工作量无疑是很重的。

如果要搞明白这个案例的解决方案以及抽象和细节之间的依赖关系,需要先了解另一个原则 --- 里氏代换原则

里氏代换原则

所谓的里氏代换原则就是子类类型必须能够替换掉它们的父类类型。

关于这个原理的应用其实也很常见,比如在Qt中,所有窗口类型的类的构造函数都有一个QWidget*类型的参数(QWidget 类是所有窗口的基类),通过这个参数指定当前窗口的父对象。虽然参数是窗口类的基类类型,但是我们在给其指定实参的大多数时候,指定的都是子类的对象,其实也就是相当于使用子类类型替换掉了它们的父类类型。

这个原则的要满足的第一个条件就是继承,其次还要求子类继承的所有父类的属性和方法对于子类来说都是合理的。关于这个是否合理下面举个栗子:

比如,对于哺乳动物来说都是胎生,但是有一种特殊的存在就是鸭嘴兽,它虽然是哺乳动物,但是是卵生。

如果我们设计了两个类:哺乳动物类和鸭嘴兽类,此时能够让鸭嘴兽类继承哺乳动物类吗?答案肯定是否定的,因为如果我们这么做了,鸭嘴兽就继承了胎生属性,这个属性和它自身的情况是不匹配的。如果想要遵循里氏代换原则,我们就不能让着两个类有继承关系。

如果我们创建了其它 的胎生的哺乳动物类,那么它们是可以继承哺乳动物这个类的,在实际应用中就可以使用子类替换掉父类,同时功能也不会受到影响,父类实现了复用,子类也能在父类的基础上增加新的行为,这个就是里氏代换原则

上面在讲依赖倒转原则的时候说过,抽象不应该依赖细节,细节应该依赖抽象。也就意味着我们应该对细节进行封装,在C++中就是将其放到一个抽象类中(C++中没有接口,不能像Java一样封装成接口),每个细节就相当于上面例子中的哺乳动物的一个特性,这样一来这个抽象的哺乳动物类就成了项目架构中高层和低层的桥梁,将二者整合到一起。

  • 抽象类中提供的接口是固定不变的
  • 低层模块是抽象类的子类,继承了抽象类的接口,并且可以重写这些接口的行为
  • 高层模块想要实现某些功能,调用的是抽象类中的函数接口,并且是通过抽象类的父类指针引用其子类的实例对象(用子类类型替换父类类型),这样就实现了多态。

基于依赖倒转原则将项目的结构换成上图的这种模式之后,低层模块发生变化,对应高层模块是没有任何影响的,这样程序猿的工作量降低了,代码也更容易维护(说白了,依赖倒转原则就是对多态的典型应用)。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/three-principles/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式

2.创建型模式

1.单例模式【巴基速递】

1. 巴基的订单

在海贼世界中,巴基速递是巴基依靠手下强大的越狱犯兵力,组建的集团海贼派遣公司,它的主要业务是向世界有需要的地方输送雇佣兵(其实是不干好事儿)。

自从从特拉法尔加罗路飞同盟击败了堂吉诃德家族 ,战争的市场对雇佣兵的依赖越来越大。订单便源源不断的来了。此时我们来分析一个问题:巴基是怎么接单并且派单的呢?

简单来说,巴基肯定是有一个账本用于记录下单者信息,下单者的需求以及下单的时间,然后根据下单的先后顺序选择合适的人手进行派单。从程序猿的视角可以这样认为,这个账本其实就相当于一个任务队列:

  • 有一定的容量,可以存储任务
  • 按照下单的先后顺序存储并处理任务 -- 典型的队列特性:先进先出

对于巴基来说把所有的订单全部记录到一个账本上就够了,如果将其平移到项目中,也就意味着应用程序在运行过程中存储任务的任务队列一个足矣,弄太多反而冗余,不太好处理了。

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

2. 独生子女

如果使用单例模式,首先要保证这个类的实例有且仅有一个,也就是说这个对象是独生子女,如果我们实施计划生育只生一个孩子,不需要也不能给再他增加兄弟姐妹。因此,就必须采取一系列的防护措施。对于类来说以上描述同样适用。涉及一个类多对象操作的函数有以下几个:

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

为了把一个类可以实例化多个对象的路堵死,可以做如下处理:

  1. 构造函数私有化,在类内部只调用一次,这个是可控的。
    • 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为私有的。
    • 在类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
  2. 拷贝构造函数私有化或者禁用(使用 = delete
  3. 拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲这个函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理一下。

由于单例模式就是给类创建一个唯一的实例对象,所以它的UML类图是很简单的:

因此,定义一个单例模式的类的示例代码如下:

C++
// 定义一个单例模式的类
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;
};

在实现一个单例模式的类的时候,有两种处理模式:

  • 饿汉模式
  • 懒汉模式

3. 饿汉模式

饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。关于这个饿汉模式的类的定义如下:

C++
// 饿汉模式
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();
}

第17行,定义这个单例类的时候,就把这个静态的单例对象创建出来了。当使用者通过getInstance()获取这个单例对象的时候,它已经被准备好了。

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

4. 懒汉模式

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

4.1 懒汉模式类的定义

C++
// 懒汉模式
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个,很显然这与单例模式的定义是相悖的。

4.2 线程安全问题

双重检查锁定

对于饿汉模式是没有线程安全问题的,在这种模式下访问单例对象的时候,这个对象已经被创建出来了。要解决懒汉模式的线程安全问题,最常用的解决方案就是使用互斥锁。可以将创建单例对象的代码使用互斥锁锁住,处理代码如下:

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

在上面代码的10~13 行这个代码块被互斥锁锁住了,也就意味着不论有多少个线程,同时执行这个代码块的线程只能是一个(相当于是严重限行了,在重负载情况下,可能导致响应缓慢)。我们可以将代码再优化一下:

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

改进的思路就是在加锁、解锁的代码块外层有添加了一个if判断(第9行),这样当任务队列的实例被创建出来之后,访问这个对象的线程就不会再执行加锁和解锁操作了(只要有了单例类的实例对象,限行就解除了),对于第一次创建单例对象的时候线程之间还是具有竞争关系,被互斥锁阻塞。上面这种通过两个嵌套的 if 来判断单例对象是否为空的操作就叫做双重检查锁定。

双重检查锁定的问题

假设有两个线程A、B,当线程A 执行到第 8 行时在线程A中 TaskQueue 实例对象 被创建,并赋值给 m_taskQ

C++
static TaskQueue* getInstance()
{
    if (m_taskQ == nullptr)
    {
        m_mutex.lock();
        if (m_taskQ == nullptr)
        {
            m_taskQ = new TaskQueue;
        }
        m_mutex.unlock();
    }
    return m_taskQ;
}

但是实际上 m_taskQ = new TaskQueue; 在执行过程中对应的机器指令可能会被重新排序。正常过程如下:

  • 第一步:分配内存用于保存 TaskQueue 对象。
  • 第二步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。
  • 第三步:使用 m_taskQ 指针指向分配的内存。

但是被重新排序以后执行顺序可能会变成这样:

  • 第一步:分配内存用于保存 TaskQueue 对象。
  • 第二步:使用 m_taskQ 指针指向分配的内存。
  • 第三步:在分配的内存中构造一个 TaskQueue 对象(初始化内存)。

这样重排序并不影响单线程的执行结果,但是在多线程中就会出问题。如果线程A按照第二种顺序执行机器指令,执行完前两步之后失去CPU时间片被挂起了,此时线程B在第3行处进行指针判断的时候m_taskQ 指针是不为空的,但这个指针指向的内存却没有被初始化,最后线程 B 使用了一个没有被初始化的队列对象就出问题了(出现这种情况是概率问题,需要反复的大量测试问题才可能会出现)。

在C++11中引入了原子变量atomic,通过原子变量可以实现一种更安全的懒汉模式的单例,代码如下:

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;
}

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

静态局部对象

在实现懒汉模式的单例的时候,相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题,那就是使用静态局部局部对象,对应的代码实现如下:

C++
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标准中有如下规定,并且这个操作是在编译时由编译器保证的:

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

最后总结一下懒汉模式和饿汉模式的区别:

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

5. 替巴基写一个任务队列

作为程序猿的我们,如果想给巴基的账本升级成一个应用程序,首要任务就是设计一个单例模式的任务队列,那么就需要赋予这个类一些属性和方法:

  1. 属性:
    • 存储任务的容器,这个容器可以选择使用STL中的队列(queue)
    • 互斥锁,多线程访问的时候用于保护任务队列中的数据
  2. 方法:主要是对任务队列中的任务进行操作
    • 任务队列中任务是否为空
    • 往任务队列中添加一个任务
    • 从任务队列中取出一个任务
    • 从任务队列中删除一个任务

根据分析,就可以把这个饿汉模式的任务队列的单例类定义出来了:

C++

#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;

class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        return &m_obj;
    }
    // 任务队列是否为空
    bool isEmpty()
    {
        lock_guard<mutex> locker(m_mutex);
        bool flag = m_taskQ.empty();
        return flag;
    }
    // 添加任务
    void addTask(int data)
    {
        lock_guard<mutex> locker(m_mutex);
        m_taskQ.push(data);
    }
    // 取出一个任务
    int takeTask()
    {
        lock_guard<mutex> locker(m_mutex);
        if (!m_taskQ.empty())
        {
            return m_taskQ.front();
        }
        return -1;
    }
    // 删除一个任务
    bool popTask()
    {
        lock_guard<mutex> locker(m_mutex);
        if (!m_taskQ.empty())
        {
            m_taskQ.pop();
            return true;
        }
        return false;
    }
private:
    TaskQueue() = default;
    static TaskQueue m_obj;
    queue<int> m_taskQ;
    mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;

int main()
{
    thread t1([]() {
        TaskQueue* taskQ = TaskQueue::getInstance();
        for (int i = 0; i < 100; ++i)
        {
            taskQ->addTask(i + 100);
            cout << "+++push task: " << i + 100 << ", threadID: " 
                << this_thread::get_id() << endl;
            this_thread::sleep_for(chrono::milliseconds(500));
        }
    });
    thread t2([]() {
        TaskQueue* taskQ = TaskQueue::getInstance();
        this_thread::sleep_for(chrono::milliseconds(100));
        while (!taskQ->isEmpty())
        {
            int data = taskQ->takeTask();
            cout << "---take task: " << data << ", threadID: " 
                << this_thread::get_id() << endl;
            taskQ->popTask();
            this_thread::sleep_for(chrono::seconds(1));
        }
    });
    t1.join();
    t2.join();
}

在上面的程序中有以下几点需要说明一下:

  • 正常情况下,任务队列中的任务应该是一个函数指针(这个指针指向的函数中有需要执行的任务动作),此处进行了简化,用一个整形数代替了任务队列中的任务。
  • 任务队列中的互斥锁保护的是单例对象的中的数据也就是任务队列中的数据,上面所说的线程安全指的是在创建单例对象的时候要保证这个对象只被创建一次,和此处完全是两码事儿,需要区别看待。
  • 关于lock_guard的使用不懂的可以跳转到这里。
  • main()函数中创建了两个子线程
    • 关于chrono库的使用不懂的可以跳转到这里。
    • 关于thread 类的使用不懂的可以跳转到这里。
    • 关于this_thread 命名空间的使用不懂的可以跳转到这里。
    • t1线程的处理动作是往任务队列中添加任务,t2线程的处理动作是从任务队列中取任务,为了保证能够取出所有的任务,此处需要让t2线程的执行晚并且慢一些。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/singleton/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


2.简单工厂模式【人造恶魔果实工

1. 工厂模式的特点

在海贼王中,作为原王下七武海之一的多弗朗明哥,可以说是新世界最大的流氓头子,拥有无上的权利和无尽的财富。他既是德雷斯罗萨国王又是地下世界的中介,控制着世界各地的诸多产业,人造恶魔果实工厂就是其中之一。

人造恶魔果实的最大买家是四皇之一的凯多凯多其实很明智,他没有自己去生产,可能有这么几个因素:

  1. 凯多手下没有像凯撒·库朗一样的科学家,无法掌握生产人造恶魔果实这种顶级的科学技术【意味着构造一个对象有时候需要经历一个非常复杂的操作流程,既然麻烦那索性就不干了】。
  2. 有需求下单就行,只需关心结果,无需关心过程【实现了解耦合】。
  3. 人造恶魔果实出了问题,自己无责任,售后直接找明哥【便于维护】。

在我们现实生活中也是一样,买馒头和自己蒸馒头、去饭店点一份大盘鸡和自己养鸡,杀鸡,做大盘鸡,这是全然不同的两种体验:

  • 自己做麻烦,而且有失败的风险,需要自己承担后果。
  • 买现成的,可以忽略制作细节,方便快捷并且无风险,得到的肯定是美味的食物。

对于后者,就相当于是一个加工厂,通过这个工厂我们就可以得到想要的东西,在程序设计中,这种模式就叫做工厂模式,工厂生成出的产品就是某个类的实例,也就是对象。关于工厂模式一共有三种,分别是:简单工厂模式、工厂模式、抽象工厂模式。

通过上面人造恶魔果实的例子,我们能够了解到,不论使用哪种工厂模式其主要目的都是实现类与类之间的解耦合,这样我们在创建对象的时候就变成了拿来主义,使程序更加便于维护。在本节中,先介绍简单工厂模式。

基于简单工厂模式去创建对象的时候,需要提供一个工厂类,专门用于生产需要的对象,这样关于对象的创建操作就被剥离出去了。

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

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

2. 生产的产品

在海贼世界中,凯撒·库朗研制的人造恶魔果实是有瑕疵的,吃下人造恶魔果实的失败品没能成功获得果实能力的人,会被剥夺除笑以外的一切表情,所以人造恶魔果实被称为SMILE

下面是明哥的SMILE工厂要生产的众多人造动物系恶魔果实中的三种:

C++
// 人造恶魔果实· 绵羊形态
class SheepSmile
{
public:
    void transform()
    {
        cout << "变成人兽 -- 山羊人形态..." << endl;
    }
    void ability()
    {
        cout << "将手臂变成绵羊角的招式 -- 巨羊角" << endl;
    }
};

// 人造恶魔果实· 狮子形态
class LionSmile
{
public:
    void transform()
    {
        cout << "变成人兽 -- 狮子人形态..." << endl;
    }
    void ability()
    {
        cout << "火遁· 豪火球之术..." << endl;
    }
};

// 人造恶魔果实· 蝙蝠形态
class BatSmile
{
public:
    void transform()
    {
        cout << "变成人兽 -- 蝙蝠人形态..." << endl;
    }
    void ability()
    {
        cout << "声纳引箭之万剑归宗..." << endl;
    }
};

不论是吃了那种恶魔果实,获得了相应的能力之后,可以做的事情大体是相同的,那就是形态变化transform() 和 使用果实能力alility()

另外,生产这些恶魔果实的时候可能需要极其复杂的参数,在此就省略了【也就是说这些类的构造函数的参数在此被省略了】。

3. 如何生产

如果想要生产出这些恶魔果实,可以先创建一个工厂类,然后再给这个工厂类添加一个工厂函数,又因为我们要生成三种不同类型的恶魔果实,所以可以给工厂函数添加一个参数,用以控制当前要生产的是哪一类。

C++
enum class Type:char{SHEEP, LION, BAT};
// 恶魔果实工厂类
class SmileFactory
{
public:
    enum class Type:char{SHEEP, LION, BAT};
    SmileFactory() {}
    ~SmileFactory() {}
    void* createSmile(Type type)
    {
        void* ptr = nullptr;
        switch (type)
        {
        case Type::SHEEP:
            ptr = new SheepSmile;
            break;
        case Type::LION:
            ptr = new LionSmile;
            break;
        case Type::BAT:
            ptr = new BatSmile;
            break;
        default:
            break;
        }
        return ptr;
    }
};

int main()
{
    SmileFactory* factory = new SmileFactory;
    BatSmile* batObj = (BatSmile*)factory->createSmile(Type::BAT);
    return 0;
}
  • 关于恶魔果实的类型,上面的类中用到了强类型枚举(C++11新特性),增强了代码的可读性,并且将枚举元素设置为了char类型,节省了内存。
  • 函数createSmile(Type type)的返回值是void*类型,这样处理主要是因为每个case 语句创建出的对象类型是不一样的,为了实现兼容,故此这样处理。
  • 得到函数createSmile(Type type)的返回值之后,还需要将其转换成实际的类型,处理起来还是比较繁琐的。

关于工厂函数的返回值,在C++中还有一种更好的解决方案,就是使用多态。如果想要实现多态,需要满足三个条件:

  • 类和类之间有继承关系。
  • 父类中有虚函数,并且在子类中需要重写这些虚函数。
  • 使用父类指针或引用指向子类对象。

所以,我们需要给人造恶魔果实提供一个基类,然后让上边的三个类SheepSmileLionSmileBatSmile作为子类继承这个基类。根据分析我们就有画出简单工厂模式的UML类图了:

根据UML类图,编写出的代码如下:

C++

#include <iostream>
using namespace std;

class AbstractSmile
{
public:
    virtual void transform() {}
    virtual void ability() {}
    virtual ~AbstractSmile() {}
};
// 人造恶魔果实· 绵羊形态
class SheepSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 山羊人形态..." << endl;
    }
    void ability() override
    {
        cout << "将手臂变成绵羊角的招式 -- 巨羊角" << endl;
    }
};

// 人造恶魔果实· 狮子形态
class LionSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 狮子人形态..." << endl;
    }
    void ability() override
    {
        cout << "火遁· 豪火球之术..." << endl;
    }
};

class BatSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 蝙蝠人形态..." << endl;
    }
    void ability() override
    {
        cout << "声纳引箭之万剑归宗..." << endl;
    }
};

// 恶魔果实工厂类
enum class Type:char{SHEEP, LION, BAT};
class SmileFactory
{
public:
    SmileFactory() {}
    ~SmileFactory() {}
    AbstractSmile* createSmile(Type type)
    {
        AbstractSmile* ptr = nullptr;
        switch (type)
        {
        case Type::SHEEP:
            ptr = new SheepSmile;
            break;
        case Type::LION:
            ptr = new LionSmile;
            break;
        case Type::BAT:
            ptr = new BatSmile;
            break;
        default:
            break;
        }
        return ptr;
    }
};

int main()
{
    SmileFactory* factory = new SmileFactory;
    AbstractSmile* obj = factory->createSmile(Type::BAT);
    obj->transform();
    obj->ability();
    return 0;
}

通过上面的代码,我们实现了一个简单工厂模式,关于里边的细节有以下几点需要说明:

  1. 由于人造恶魔果实类有继承关系, 并且实现了多态,所以父类的析构函数也应该是虚函数,这样才能够通过父类指针或引用析构子类的对象。

  2. 工厂函数createSmile(Type type)的返回值修改成了AbstractSmile*类型,这是人造恶魔果实类的基类,通过这个指针保存的是子类对象的地址,这样就实现了多态,所以在main()函数中,通过obj对象调用的实际是子类BatSmile中的函数,因此打印出的信息应该是这样的:

    C++
    变成人兽 -- 蝙蝠人形态...
    声纳引箭之万剑归宗...
    

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/simple-factory/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


3.工厂模式[人造恶魔果实工厂2]

1. 简单工厂模式的弊端

在上一节简单工厂模式中,创建了一个工厂类,用于生产需要的对象,但是这种方式有一个弊端,它违反了设计模式中的开放-封闭原则,先来看相关的代码:

C++
// 恶魔果实工厂类
enum class Type:char{SHEEP, LION, BAT};
class SmileFactory
{
public:
    SmileFactory() {}
    ~SmileFactory() {}
    AbstractSmile* createSmile(Type type)
    {
        AbstractSmile* ptr = nullptr;
        switch (type)
        {
        case Type::SHEEP:
            ptr = new SheepSmile;
            break;
        case Type::LION:
            ptr = new LionSmile;
            break;
        case Type::BAT:
            ptr = new BatSmile;
            break;
        default:
            break;
        }
        return ptr;
    }
};

在上面的工厂函数中需要生成三种人造恶魔果实,现在如果想要生成更多,那么就需要在工厂函数的switch语句中添加更多的case,很明显这违背了封闭原则,也就意味着需要基于开放原则来解决这个问题。

使用工厂模式可以很完美的解决上述的问题,简单工厂模式是只有一个工厂类,而工厂模式是有很多的工厂类:

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

2. 工厂模式

我们先修改一下简单工厂模式中工厂类相关的代码:

C++

// 恶魔果实工厂类
class AbstractFactory
{
public:
    virtual AbstractSmile* createSmile() = 0;
    virtual ~AbstractFactory() {}
};

class SheepFactory : public AbstractFactory
{
public:
    AbstractSmile* createSmile() override
    {
        return new SheepSmile;
    }
    ~SheepFactory()
    {
        cout << "释放 SheepFactory 类相关的内存资源" << endl;
    }
};

class LionFactory : public AbstractFactory
{
public:
    AbstractSmile* createSmile() override
    {
        return new LionSmile;
    }
    ~LionFactory()
    {
        cout << "释放 LionFactory 类相关的内存资源" << endl;
    }

};

class BatFactory : public AbstractFactory
{
public:
    AbstractSmile* createSmile() override
    {
        return new BatSmile;
    }
    ~BatFactory()
    {
        cout << "释放 BatFactory 类相关的内存资源" << endl;
    }
};

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

根据简单工厂模式的代码和上面的修改就可以把工厂模式的UML类图画出来了:

完整的代码应该是这样的:

C++

#include <iostream>
using namespace std;

class AbstractSmile
{
public:
    virtual void transform() = 0;
    virtual void ability() = 0;
    virtual ~AbstractSmile() {}
};
// 人造恶魔果实· 绵羊形态
class SheepSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 山羊人形态..." << endl;
    }
    void ability() override
    {
        cout << "将手臂变成绵羊角的招式 -- 巨羊角" << endl;
    }
};

// 人造恶魔果实· 狮子形态
class LionSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 狮子人形态..." << endl;
    }
    void ability() override
    {
        cout << "火遁· 豪火球之术..." << endl;
    }
};

class BatSmile : public AbstractSmile
{
public:
    void transform() override
    {
        cout << "变成人兽 -- 蝙蝠人形态..." << endl;
    }
    void ability() override
    {
        cout << "声纳引箭之万剑归宗..." << endl;
    }
};

// 恶魔果实工厂类
class AbstractFactory
{
public:
    virtual AbstractSmile* createSmile() = 0;
    virtual ~AbstractFactory() {}
};

class SheepFactory : public AbstractFactory
{
public:
    AbstractSmile* createSmile() override
    {
        return new SheepSmile;
    }
    ~SheepFactory()
    {
        cout << "释放 SheepFactory 类相关的内存资源" << endl;
    }
};

class LionFactory : public AbstractFactory
{
public:
    // 工厂函数
    AbstractSmile* createSmile() override
    {
        return new LionSmile;
    }
    ~LionFactory()
    {
        cout << "释放 LionFactory 类相关的内存资源" << endl;
    }

};

class BatFactory : public AbstractFactory
{
public:
    // 工厂函数
    AbstractSmile* createSmile() override
    {
        return new BatSmile;
    }
    ~BatFactory()
    {
        cout << "释放 BatFactory 类相关的内存资源" << endl;
    }
};

int main()
{
    AbstractFactory* factory = new BatFactory;
    AbstractSmile* obj = factory->createSmile();
    obj->transform();
    obj->ability();
    return 0;
}

main()函数中的这句代码是实例化了一个生成蝙蝠恶魔果实的工厂对象:

C++
AbstractFactory* factory = new BatFactory;

在真实的项目场景中,要生成什么类型的恶魔果实其实是通过客户端的操作界面控制的,它对应的可能是一个按钮或者是一个选择列表,用户做出了选择,程序就可以根据该需求去创建对应的工厂对象,最终将选择的恶魔果实生产出来。

在上面的例子中,不论是恶魔果实的基类,还是工厂类的基类,它们的虚函数可以是纯虚函数,也可以是非纯虚函数。这样的基类在设计模式中就可以称之为抽象类(此处的抽象类和C++中对抽象类的定义有一点出入)。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/factory/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


4.抽像工厂模式【弗兰奇一家】

1. 奔向大海

在海贼世界中,位于水之都的弗兰奇一家是由铁人弗兰奇所领导的以拆船为职业的家族,当然了他们的逆向工程做的也很好,会拆船必然会造船。船是海贼们出海所必备的海上交通工具,它由很多的零件组成,从宏观上看它有这么几个组成部分:船体动力系统武器

有一天我攒够了钱要出海,找到了弗兰奇一家,发现他们的老大跟着草帽路飞出海了,但是我还是选择相信他们的技术。下面是他们给我制定的造船方案,根据我的购买力提供了不同型号的海贼船,一共是三个级别,如下表:

基础型 标准型 旗舰型
船体 木头 钢铁 合成金属
动力 手动 内燃机 核能
武器 速射炮 激光

根据这个表,在造船的时候需要根据不同的型号选择相应的零部件,在设计程序的时候还需要保证遵循开放-封闭原则,即添加了新型号之后不需要修改原有代码,而是添加新的代码。

1.1 船体

因为要建造的这艘船是由多个部件组成的并且每个部件还有不同的品级可供选择,先说船体,关于船体材料的这个属性是可变的,所以还需要给它提供一个抽象类,这样在这个抽象类的子类中就可以更换不同的船体材料了:

C++
// 船体
class ShipBody
{
public:
    virtual string getShipBody() = 0;
    virtual ~ShipBody() {}
};

class WoodBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<木材>制作轮船船体...");
    }
};

class IronBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<钢铁>制作轮船船体...");
    }
};

class MetalBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<合金>制作轮船船体...");
    }
};

这样,只要添加了新的造船材料,就给它添加一个对应的子类(父类是 ShipBody),在这个子类重写父类的虚函数getShipBody(),用这种材料把船体造出来就行了。

1.2 动力和武器

知道了如何处理船体部分,那么动力武器部分的处理思路也是一样的:

  • 可以给船提供不同的动力系统,因此这个属性是可变的,所以需要提供一个抽象类
  • 可以给船提供不同的武器系统,因此这个属性也是可变的,所以也需要提供一个抽象类

照葫芦画瓢把代码写一下:

C++

// 动力
class Engine
{
public:
    virtual string getEngine() = 0;
    virtual ~Engine() {}
};

class Human : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<人力驱动>...");
    }
};

class Diesel : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<内燃机驱动>...");
    }
};

class Nuclear : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<核能驱动>...");
    }
};

// 武器
class Weapon
{
public:
    virtual string getWeapon() = 0;
    virtual ~Weapon() {}
};

class Gun : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<枪>");
    }
};

class Cannon : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<自动机关炮>");
    }
};

class Laser : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<激光>");
    }
};

不论是动力还是武器系统都是需要提供一个抽象类,这样它们的子类就可以基于这个抽象基类进行专门定制,如果要对它们进行拓展也只需添加新的类,不需要修改原有代码。

1.3 一艘船

如果有了以上的零件,只需要在工厂中将它们装配到一起,这样就得到了一艘船,这是一艘什么型号的船取决于使用的是什么零件,所以只需要让这艘船对应一个类就可以了,这个类的定义如下:

C++
// 轮船类
class Ship
{
public:
    Ship(ShipBody* body, Weapon* weapon, Engine* engine) :
        m_body(body), m_weapon(weapon), m_engine(engine) 
    {
    }
    string getProperty()
    {
        string info = m_body->getShipBody() + m_weapon->getWeapon() + m_engine->getEngine();
        return info;
    }
    ~Ship() 
    {
        delete m_body;
        delete m_engine;
        delete m_weapon;
    }
private:
    ShipBody* m_body = nullptr;
    Weapon* m_weapon = nullptr;
    Engine* m_engine = nullptr;
};

这艘船使用的零件是通过构造函数参数传递进来的,并在类的内部对这些零件对象进行了保存,这样在释放船这个对象的时候就可以将相应的零件对象一并析构了。

另外,在Ship这个类中保存零件对象的时候使用的是它们的父类指针,这样就可以实现多态了。

2. 准备生产

万事俱备,只剩建厂了。造船厂要生产三种型号的船,那么也就是至少需要三条生产线,所以对应的工厂类也就不止一个,处理思路还是一样的,提供一个抽象的基类,然后在它的子类中完成各种型号的船的组装,每个子类对应的就是一条生产线。

2.1 设计图纸

现在,关于抽象工厂模式的逻辑应该是比较清晰了,下面来看一下这个模式对应的UML类图:

在这个图中有四个抽象类,分别是:

   ShipBody 类

:船体的抽象类

  • 有三个子类,在子类中通过不同的材料来建造船体
   Weapon 类

:武器的抽象类

  • 有三个子类,在子类中给战船提供不同种类的武器
   Engine 类

:动力系统抽象类

  • 有三个子类,在子类中给战船提供不同动力系统
   AbstractFactory 类

:抽象工厂类

  • 在子工厂类中生产不同型号的战船
  • ShipBody WeaponEngine有依赖关系,在工厂函数中创建了它们的实例对象
  • Ship 类有依赖关系,在工厂函数中创建了它的实例对象

关于Ship类它可以和ShipBody WeaponEngine可以是聚合关系,也可以是组合关系:

  • 组合关系:析构Ship类对象的时候,也释放了ShipBody WeaponEngine对象
  • 聚合关系:析构Ship类对象的时候,没有释放ShipBody WeaponEngine对象

在上面的Ship类的析构函数中做了释放操作,因此在UML中将它们之间描述为了组合关系。

在使用抽象工厂模式来处理实际问题的时候,由于实际需求不一样,我们画出的UML类图中有些类和类之间的关系可能也会有所不同,所以上图只适用于当前的业务场景,在处理其他需求的时候还需要具体问题具体分析。

2.2 开工

给上面的程序再添加相应的工厂类,就可以生产出我们需要的型号的船只了,示例代码如下:

C++

#include <iostream>
#include <string>
using namespace std;

// 船体
class ShipBody
{
public:
    virtual string getShipBody() = 0;
    virtual ~ShipBody() {}
};

class WoodBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<木材>制作轮船船体...");
    }
};

class IronBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<钢铁>制作轮船船体...");
    }
};

class MetalBody : public ShipBody
{
public:
    string getShipBody() override
    {
        return string("用<合金>制作轮船船体...");
    }
};

// 武器
class Weapon
{
public:
    virtual string getWeapon() = 0;
    virtual ~Weapon() {}
};

class Gun : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<枪>...");
    }
};

class Cannon : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<自动机关炮>...");
    }
};

class Laser : public Weapon
{
public:
    string getWeapon() override
    {
        return string("配备的武器是<激光>...");
    }
};

// 动力
class Engine
{
public:
    virtual string getEngine() = 0;
    virtual ~Engine() {}
};

class Human : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<人力驱动>...");
    }
};

class Diesel : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<内燃机驱动>...");
    }
};

class Nuclear : public Engine
{
public:
    string getEngine() override
    {
        return string("使用<核能驱动>...");
    }
};

// 轮船类
class Ship
{
public:
    Ship(ShipBody* body, Weapon* weapon, Engine* engine) :
        m_body(body), m_weapon(weapon), m_engine(engine) 
    {
    }
    string getProperty()
    {
        string info = m_body->getShipBody() + m_weapon->getWeapon() + m_engine->getEngine();
        return info;
    }
    ~Ship() 
    {
        delete m_body;
        delete m_engine;
        delete m_weapon;
    }
private:
    ShipBody* m_body = nullptr;
    Weapon* m_weapon = nullptr;
    Engine* m_engine = nullptr;
};

// 工厂类
class AbstractFactory
{
public:
    virtual Ship* createShip() = 0;
    virtual ~AbstractFactory() {}
};

class BasicFactory : public AbstractFactory
{
public:
    Ship* createShip() override
    {
        Ship* ship = new Ship(new WoodBody, new Gun, new Human);
        cout << "<基础型>战船生产完毕, 可以下水啦..." << endl;
        return ship;
    }
};

class StandardFactory : public AbstractFactory
{
public:
    Ship* createShip() override
    {
        Ship* ship = new Ship(new IronBody, new Cannon, new Diesel);
        cout << "<标准型>战船生产完毕, 可以下水啦..." << endl;
        return ship;
    }
};

class UltimateFactory : public AbstractFactory
{
public:
    Ship* createShip() override
    {
        Ship* ship = new Ship(new MetalBody, new Laser, new Nuclear);
        cout << "<旗舰型>战船生产完毕, 可以下水啦..." << endl;
        return ship;
    }
};

int main()
{
    AbstractFactory* factroy = new StandardFactory;
    Ship* ship = factroy->createShip();
    cout << ship->getProperty();
    delete ship;
    delete factroy;
    return 0;
}

main()函数中,要通过工厂类的工厂函数生产什么型号的战船,和用户的需求息息相关,所以这个选择也是用户通过客户端的操作界面做出的,在这个例子中,关于客户端的界面操作就直接忽略了。

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

关于简单工厂模式、工厂模式和抽象工厂模式的区别可以做如下总结:

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/abstract-factory/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


5.生成器/建造者模式【卡雷拉公司】

1. 造船,我是专业的

在海贼世界中,水之都拥有全世界最好的造船技术,三大古代兵器之一的冥王就是由岛上的造船技师们制造出来的。现在岛上最大、最优秀的造船公司就是卡雷拉公司,它的老板还是水之都的市长,财富权力他都有,妥妥的人生赢家。

众所周知,在冰山身边潜伏着很多卧底,他们都是世界政府直属秘密谍报机关 CP9成员,目的是要得到古代兵器冥王的设计图,但是很不幸图纸后来被弗兰奇烧掉了。既然他们造船这么厉害,我也来到了卡雷拉公司,学习一下他们是怎么造船的。

1.1 桑尼号

以下是我拿到的冰山和弗兰奇给路飞造的桑尼号的部分设计图纸:

通过图纸可以感受到造一艘船的工序是极其复杂的,看到这儿,曾经作为程序猿的我又想到了写程序,如果只通过一个类直接把这种结构的船构建出来完全是不可能,先不说别的光构造函数的参数就已经不计其数了。

冰山给出的解决方案是化繁为简,逐个击破。也就是分步骤创建复杂的对象,并且允许使用相同的代码生成不同类型和形式的对象,他说这种模式叫做生成器模式(也叫建造者模式)

1.2 生成器

生成器模式建议将造船工序的代码从产品类中抽取出来, 并将其放在一个名为生成器的独立类中。

在这个生成器类中有一系列的构建步骤,每次造船的时候,只需从中选择需要的步骤并调用就可以得到满足需求的实例对象。

假设我们要通过上面的生成器建造很多不同规格、型号的海贼船,那么就需要创建多个生成器,但是有一点是不变的:生成器内部的构建步骤不变。

简约型 标准型 豪华型
船体
动力
武器
内室 毛坯 毛坯 精装

比如,我想建造两种型号的海贼船:桑尼号梅利号,并且按照上面的三个规格,每种造一艘,此时就需要两个生成器:桑尼号生成器梅利号生成器,并且这两个生成器还需要对应一个父类,父类生成器中的建造函数应该设置为虚函数。

1.3 主管

冰山说可以进一步将用于创建产品的一系列生成器步骤调用抽取成为单独的主管类。 主管类可定义创建步骤的执行顺序, 而生成器则提供这些步骤的实现。

严格来说, 程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。

此外, 对于客户端代码来说, 主管类完全隐藏了产品构造细节。 客户端只需要将一个生成器与主管类关联, 然后使用主管类来构造产品, 就能从生成器处获得构造结果了。

2. 说干就干

2.1 船模型

现在我们开始着手把路飞的海贼船桑尼号梅利号使用生成器模式键造出来。

  • 一共需要三个生成器类,一共父类,两个子类
  • 父类可以是一个抽象类,提供的建造函数都是虚函数
  • 在两个生成器子类中,使用建造函数分别将桑尼号梅利号各个零部件造出来。

如果我们仔细分析,发现还需要解决另外一个问题,通过生成器得到了海贼船的各个零部件,这些零部件必须有一个载体,那就是海贼船对象。因此,还需要提供一个或多个海贼船类。

因为桑尼号梅利号这两艘的差别非常之巨大,所以我们定义两个海贼船类,代码如下:

C++
// 桑尼号
class SunnyShip
{
public:
    // 添加零件
    void addParts(string name)
    {
        m_parts.push_back(name);
    }
    void showParts()
    {
        for (const auto& item : m_parts)
        {
            cout << item << "   ";
        }
        cout << endl;
    }
private:
    vector<string> m_parts;
};

// 梅利号
class MerryShip
{
public:
    // 组装
    void assemble(string name, string parts)
    {
        m_patrs.insert(make_pair(name, parts));
    }
    void showParts()
    {
        for (const auto& item : m_patrs)
        {
            cout << item.first << ": " << item.second << "  ";
        }
        cout << endl;
    }
private:
    map<string, string> m_patrs;
};

在上面的两个类中,通过一个字符串来代表某个零部件,为了使这两个类有区别SunnyShip 类中使用vector 容器存储数据,MerryShip 类中使用map 容器存储数据。

2.2 船生成器

虽然有海贼船类,但是这两个海贼船类并不造船,每艘船的零部件都是由他们对应的生成器类构建完成的,下面是生成器类的代码:

抽象生成器

C++
// 生成器类
class ShipBuilder
{
public:
    virtual void reset() = 0;
    virtual void buildBody() = 0;
    virtual void buildWeapon() = 0;
    virtual void buildEngine() = 0;
    virtual void buildInterior() = 0;
    virtual ~ShipBuilder() {}
};

在这个抽象类中定义了建造海贼船所有零部件的方法,在这个类的子类中需要重写这些虚函数,分别完成桑尼号梅利号零件的建造。

桑尼号生成器

C++
// 桑尼号生成器
class SunnyBuilder : public ShipBuilder
{
public:
    SunnyBuilder()
    {
        reset();
    }
    ~SunnyBuilder()
    {
        if (m_sunny != nullptr)
        {
            delete m_sunny;
        }
    }
    // 提供重置函数, 目的是能够使用生成器对象生成多个产品
    void reset() override
    {
        m_sunny = new SunnyShip;
    }
    void buildBody() override
    {
        m_sunny->addParts("神树亚当的树干");
    }
    void buildWeapon() override
    {
        m_sunny->addParts("狮吼炮");
    }
    void buildEngine() override
    {
        m_sunny->addParts("可乐驱动");
    }
    void buildInterior() override
    {
        m_sunny->addParts("豪华内室精装");
    }
    SunnyShip* getSunny()
    {
        SunnyShip* ship = m_sunny;
        m_sunny = nullptr;
        return ship;
    }
private:
    SunnyShip* m_sunny = nullptr;
};

在这个生成器类中只要调用build 方法,对应的零件就会被加载到SunnyShip 类的对象 m_sunny 中,当船被造好之后就可以通过SunnyShip* getSunny()方法得到桑尼号的实例对象,当这个对象地址被外部指针接管之后,当前生成器类就不会再维护其内存的释放了。如果想通过生成器对象建造第二艘桑尼号就可以调用这个类的reset()方法,这样就得到了一个新的桑尼号对象,之后再调用相应的建造函数,这个对象就被初始化了。

梅利号生成器

C++
// 梅利号生成器
class MerryBuilder : public ShipBuilder
{
public:
    MerryBuilder()
    {
        reset();
    }
    ~MerryBuilder()
    {
        if (m_merry != nullptr)
        {
            delete m_merry;
        }
    }
    void reset() override
    {
        m_merry = new MerryShip;
    }
    void buildBody() override
    {
        m_merry->assemble("船体", "优质木材");
    }
    void buildWeapon() override
    {
        m_merry->assemble("武器", "四门大炮");
    }
    void buildEngine() override
    {
        m_merry->assemble("动力", "蒸汽机");
    }
    void buildInterior() override
    {
        m_merry->assemble("内室", "精装");
    }
    MerryShip* getMerry()
    {
        MerryShip* ship = m_merry;
        m_merry = nullptr;
        return ship;
    }
private:
    MerryShip* m_merry = nullptr;
};

梅利号的生成器和桑尼号的生成器内部做的事情是一样的,在此就不过多赘述了。

2.3 包工头

如果想要隐藏造船细节,就可以添加一个主管类,这个主管类就相当于一个包工头,脏活累活他都干了,我们看到的就是一个结果。

根据需求,桑尼号和梅利号分别有三个规格,简约型标准型豪华型,根据不同的规格,有选择的调用生成器中不同的建造函数,就可以得到最终的成品了。

C++
// 主管类
class Director
{
public:
    void setBuilder(ShipBuilder* builder)
    {
        m_builder = builder;
    }
    // 简约型
    void builderSimpleShip()
    {
        m_builder->buildBody();
        m_builder->buildEngine();
    }
    // 标准型
    void builderStandardShip()
    {
        builderSimpleShip();
        m_builder->buildWeapon();
    }
    // 豪华型
    void builderRegalShip()
    {
        builderStandardShip();
        m_builder->buildInterior();
    }
private:
    ShipBuilder* m_builder = nullptr;
};

在使用主管类的时候,需要通过setBuilder(ShipBuilder* builder)给它的对象传递一个生成器对象,形参是父类指针,实参应该是子类对象,这样做的目的是为了实现多态,并且在这个地方这个函数是一个传入传出参数

3. 验收

最后测试一个桑尼号和梅利号分别对应的三种规格的船能否被建造出来:

C++

// 建造桑尼号
void builderSunny()
{
    Director* director = new Director;
    SunnyBuilder* builder = new SunnyBuilder;
    // 简约型
    director->setBuilder(builder);
    director->builderSimpleShip();
    SunnyShip* sunny = builder->getSunny();
    sunny->showParts();
    delete sunny;

    // 标准型
    builder->reset();
    director->setBuilder(builder);
    director->builderStandardShip();
    sunny = builder->getSunny();
    sunny->showParts();
    delete sunny;

    // 豪华型
    builder->reset();
    director->setBuilder(builder);
    director->builderRegalShip();
    sunny = builder->getSunny();
    sunny->showParts();
    delete sunny;
    delete builder;
    delete director;
}

// 建造梅利号
void builderMerry()
{
    Director* director = new Director;
    MerryBuilder* builder = new MerryBuilder;
    // 简约型
    director->setBuilder(builder);
    director->builderSimpleShip();
    MerryShip* merry = builder->getMerry();
    merry->showParts();
    delete merry;

    // 标准型
    builder->reset();
    director->setBuilder(builder);
    director->builderStandardShip();
    merry = builder->getMerry();
    merry->showParts();
    delete merry;

    // 豪华型
    builder->reset();
    director->setBuilder(builder);
    director->builderRegalShip();
    merry = builder->getMerry();
    merry->showParts();
    delete merry;
    delete builder;
    delete director;
}

int main()
{
    builderSunny();
    cout << "=====================================" << endl;
    builderMerry();
}

程序输出:

C++
神树亚当的树干   可乐驱动
神树亚当的树干   可乐驱动   狮吼炮
神树亚当的树干   可乐驱动   狮吼炮   豪华内室精装
===================================== 
船体: 优质木材  动力: 蒸汽机
船体: 优质木材  动力: 蒸汽机  武器: 四门大炮
船体: 优质木材  动力: 蒸汽机  内室: 精装  武器: 四门大炮

可以看到,输出结果是没问题的,使用生成器模式造船成功!

4. 收工

最后根据上面的代码把UML类图画一下(在学习设计模式的时候只能最后出图,在做项目的时候应该是先画UML类图,再写程序)。

通过编写的代码可得知Director 类 ShipBuilder 类之间有两种关系依赖关联,但在描述这二者的关系的时候只能画一条线,一般会选择最紧密的那个关系,在此处就是关联关系

在这个图中,没有把使用这用这些类的客户端画出来,这个客户端对应的是上面程序中的main()函数中调用的测试代码,在真实场景中对应的应该是一个客户端操作界面,由用户做出选择,从而在程序中根据选择建造不同型号,不同规格的海贼船。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/builder/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


6.原型模式[杰尔马66]

1. 克隆人

在海贼王世界中,杰尔马王国拥有一支强大的科学作战部队 -- 杰尔马66军团,其先锋是文斯莫克家族,他们作为雇佣军活跃在世界各地。

这支部队战斗力强悍,没有情感,不畏生死,勇往直前。从某种意义上来讲,他们不能被称之为人,因为他们是科学的结晶,他们都出自文斯莫克·伽治之手。

伽治曾和海贼世界中的顶级科学家贝加庞克是同事,一起发现了血统因子,于是才有了现在这么多的杰尔马士兵,对,你没有看错他们都是被克隆出来的。

克隆是一种最直接、最快捷的创建新对象的方式,它不仅隐藏了创建新对象的诸多细节,还保留了源对象的属性信息,保证了这两个对象能够一模一样。

血统因子士兵这是一个复杂而又艰辛的过程,一旦研发成功,之后的事情就是基于母体进行复制(克隆)。我猜伽治不仅是一名科学家,可能还是一位架构师,因为他懂设计模式,这种制作克隆人的模式就是原型模式

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

2. 这是在脱了裤子放屁吗

懂C++的亲们看了上面关于原型模式的描述肯定是满脑子问号。因为在C++中只要定义一个类,这个类就默认自带六大函数,其中一个就是拷贝构造函数,这个函数的作用就是通过一个已有对象克隆出一个新的对象。一个拷贝构造函数就能搞定的事情为啥还要搞出一种设计模式呢?

这是脱了裤子放屁吗?肯定不是,因为这里边还隐藏着一个细节。

  • 对于伽治来说,他可能想通过一代士兵克隆出更优秀的二代士兵
  • 对于程序猿来说,我们可能想要通父类指针或引用把指向的子类对象克隆出来

通过这个描述,就可以从里面挖掘出一个重要的信息:克隆可能会在父类和子类之间进行,并且可能是动态的,很明显通过父类的拷贝构造函数无法实现对子类对象的拷贝,其实这就是一个多态,我们需要给父类提供一个克隆函数并且是一个虚函数

现在逻辑关系已经说明白了,来看一下对应UML类图

3. 量产士兵

根据上面的UML类图,我们就可以把对应的代码写出了,示例代码如下:

C++

#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;
}

代码中的main()函数对应的就是UML类图中的客户端角色。

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

程序执行的结果如下:

C++
我是杰尔马66的超级士兵!!!
我是杰尔马67的超级士兵!!!

通过输出的结果可以看到通过父类指针克隆子类的对象成功了。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/prototype/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


3.结构型模式

1.适配器模式【托尼托尼乔巴】

1. 翻译家

在海贼王中,托尼托尼·乔巴(Tony Tony Chopper)草帽海贼团的船医,它本来是一头驯鹿,但是误食了动物系·人人果实之后可以变成人的形态。

乔巴吃了恶魔果实之后的战斗力暂且抛开不谈,说说它掌握的第二技能:语言,此时的他既能听懂人的语言,又能听懂动物语言,妥妥的语言学家。

人和动物本来无法直接交流,但是有了乔巴的存在,就相当于了有了一条纽带,一座桥梁,使得二者之间能够顺畅的沟通。在这里边,乔巴充当的就是一个适配器,将一个类的接口转换成用户希望的另一个接口,使不兼容的对象能够相互配合并一起工作,这种模式就叫适配器模式。说白了,适配器模式就相当于找了一个翻译。

需要适配器的例子或者场景很多,随便列举几个:

  1. STL标准模板库有六大组件,其中之一的就是适配器。
    • 六大组件分别是:容器、算法、迭代器、仿函数、适配器、空间适配器。
    • 适配器又可以分为:容器适配器、函数适配器、迭代器适配器
  2. 台湾省的电压是110V,大陆是220V,如果他们把大陆的电器带回台湾就需要适配器进行电压的转换。
  3. 香港的插座插孔是欧式的,从大陆去香港旅游,就需要带转换头(适配器)
  4. 儿媳妇儿和婆婆打架,就需要儿子从中调解,此时儿子就适配器。
  5. 手机、平板、电脑等需要的电压并不是220V,也需要适配器进行转换。

2. 斜杠型人才

所谓的斜杠型人才就是多才多艺,适配器也一样,如果它能给多个不相干的对象进行相互之间的适配,这个适配器就是斜杠适配器。

还是拿乔巴举例子,他既能把人的语言翻译给动物,又能把动物的语言翻译给人,那么此时的乔巴就是一个双向适配器。

2.1 国宝的血泪史

覆巢之下无完卵,清朝末年,作为曾经蚩尤的坐骑而今呆萌可爱的国宝大熊猫惨遭西方国家围猎和杀戮,其中以美利坚尤甚。

1869年春天,法国传教士阿尔芒·戴维在四川宝兴寻找珍稀物种的时候发现了大熊猫,国宝的厄运就此开始。

这是美国总统罗斯福两个儿子制作的大熊猫标本:

那个时候的旧中国对熊猫还没有一个确切的认识,并没有采取任何措施,于是外国人对熊猫的猎杀活动更变本加厉起来。据统计,在1936年到1946年之间,超过16只活体大熊猫从中国运出,而熊猫标本更是多达70余具!

一切终成过去,作为一个程序猿我也不能改变什么,但是我决定要写个程序让这群混蛋给被他们杀死的国宝道歉!

2.2 抽丝剥茧

对于杀害大熊猫的这群西方的混蛋,不论他们怎么忏悔大熊猫肯定也是听不懂的,所以需要适配器来完成这二者之间的交流。但是这帮人来自不同的国家,它们都有自己的语言,所以这个适配器翻译的不是一种人类语言而是多种,因此我们要做如下的处理:

  1. 忏悔的人说的是不同的语言,所以需要一个抽象类。
  2. 适配器需要翻译不同国家的语言,所以适配器也需要一个抽象类。
  3. 西方人需要给大熊猫道歉,因此需要给大熊猫定义一个类。

西方罪人

先把西方这群烂人对应的类定义出来:

C++
class Foreigner
{
public:
    virtual string confession() = 0;
    void setResult(string msg)
    {
        cout << "Panda Say: " << msg << endl;
    }
    virtual ~Foreigner() {}
};

// 美国人
class American : public Foreigner
{
public:
    string confession() override
    {
        return string("我是畜生, 我有罪!!!");
    }
};

// 法国人
class French : public Foreigner
{
public:
    string confession()
    {
        return string("我是强盗, 我该死!!!");
    }
};

不同国家的西方罪人需要使用不同的语言向大熊猫忏悔,所以美国人法国人作为子类需要重写从父类继承的用于忏悔的虚函数confession()

当乔巴这个适配器翻译了熊猫的语言之后,需要通过void setResult(string msg)函数将信息传递给西方罪人对象。

大熊猫

再把国宝对应的类定义出来

C++
// 大熊猫
class Panda
{
public:
    void recvMessage(string msg)
    {
        cout << msg << endl;
    }
    string sendMessage()
    {
        return string("强盗、凶手、罪人是不可能被宽恕和原谅的!");
    }
};

大熊猫类有两个方法:

  • recvMessage(string msg):接收忏悔信息。
  • string sendMessage():告诉西方人是否原谅他们。

乔巴登场

同时能听懂人类和动物语言非乔巴莫属,由于要翻译两种不同的人类语言,所以需要一个抽象的乔巴适配器类,在其子类中完成英语 <==> 熊猫语法语 <==> 熊猫语之间的翻译。

C++

// 抽象乔巴适配器类
class AbstractChopper
{
public:
    AbstractChopper(Foreigner* foreigner) : m_foreigner(foreigner) {}
    virtual void translateToPanda() = 0;
    virtual void translateToHuman() = 0;
    virtual ~AbstractChopper() {}
protected:
    Panda m_panda;
    Foreigner* m_foreigner = nullptr;
};

// 英语乔巴适配器
class EnglishChopper : public AbstractChopper
{
public:
    // 继承构造函数
    using AbstractChopper::AbstractChopper;
    void translateToPanda() override
    {
        string msg = m_foreigner->confession();
        // 翻译并将信息传递给熊猫对象
        m_panda.recvMessage("美国人说: " + msg);
    }
    void translateToHuman() override
    {
        // 接收熊猫的信息
        string msg = m_panda.sendMessage();
        // 翻译并将熊猫的话转发给美国人
        m_foreigner->setResult("美国佬, " + msg);
    }
};

// 法语乔巴适配器
class FrenchChopper : public AbstractChopper
{
public:
    using AbstractChopper::AbstractChopper;
    void translateToPanda() override
    {
        string msg = m_foreigner->confession();
        // 翻译并将信息传递给熊猫对象
        m_panda.recvMessage("法国人说: " + msg);
    }
    void translateToHuman() override
    {
        // 接收熊猫的信息
        string msg = m_panda.sendMessage();
        // 翻译并将熊猫的话转发给法国人
        m_foreigner->setResult("法国佬, " + msg);
    }
};

在上面的适配器类中,同时访问了Foreigner 类Panda 类,这样适配器类就可以拿到这两个类对象中的数据进行转译,最后再将其分别发送给对方,这样这两个不相干的没有交集的类对象之间就可以正常的沟通了。

不可原谅

最后编写程序进行测试,这部分程序其实是通过客户端的操作并被执行的,此处就将其直接写到main()函数中了:

C++
int main()
{
    Foreigner* human = new American;
    EnglishChopper* american = new EnglishChopper(human);
    american->translateToPanda();
    american->translateToHuman();
    delete human;
    delete american;

    human = new French;
    FrenchChopper* french = new FrenchChopper(human);
    french->translateToPanda();
    french->translateToHuman();
    delete human;
    delete french;

    return 0;
}

程序输出的结果:

C++
美国人说: 我是畜生, 我有罪!!!
Panda Say: 美国佬, 强盗、凶手、罪人是不可能被宽恕和原谅的!
============================
法国人说: 我是强盗, 我该死!!!
Panda Say: 法国佬, 强盗、凶手、罪人是不可能被宽恕和原谅的!

3. 结构图

最后根据上面的代码,把对应的UML类图画一下(再次强调,UML类图是在在写程序之前画的,用来梳理程序的设计思路。学会了设计模式之后,就需要在写程序之前画类图了。)

在这个UML类图中,将抽象的乔巴类(抽象适配器类)熊猫类设置为了关联关系,除了使用这种方式我们还可以让抽象的适配器类继承熊猫类,这样在适配器类中就可以直接使用熊猫类中定义的方法了,如下图:

上图对应的代码如下:

C++

class Foreigner
{
public:
    virtual string confession() = 0;
    void setResult(string msg)
    {
        cout << "Panda Say: " << msg << endl;
    }
    virtual ~Foreigner() {}
};

// 美国人
class American : public Foreigner
{
public:
    string confession() override
    {
        return string("我是畜生, 我有罪!!!");
    }
};

// 法国人
class French : public Foreigner
{
public:
    string confession()
    {
        return string("我是强盗, 我该死!!!");
    }
};

// 大熊猫
class Panda
{
public:
    void recvMessage(string msg)
    {
        cout << msg << endl;
    }
    string sendMessage()
    {
        return string("强盗、凶手、罪人是不可能被宽恕和原谅的!");
    }
};

// 抽象适配器类
class AbstractChopper : public Panda
{
public:
    AbstractChopper(Foreigner* foreigner) : m_foreigner(foreigner) {}
    virtual void translateToPanda() = 0;
    virtual void translateToHuman() = 0;
    virtual ~AbstractChopper() {}
protected:
    Foreigner* m_foreigner = nullptr;
};

class EnglishChopper : public AbstractChopper
{
public:
    using AbstractChopper::AbstractChopper;
    void translateToPanda() override
    {
        string msg = m_foreigner->confession();
        // 翻译并将信息传递给熊猫对象
        recvMessage("美国人说: " + msg);
    }
    void translateToHuman() override
    {
        // 接收熊猫的信息
        string msg = sendMessage();
        // 翻译并将熊猫的话转发给美国人
        m_foreigner->setResult("美国佬, " + msg);
    }
};

class FrenchChopper : public AbstractChopper
{
public:
    using AbstractChopper::AbstractChopper;
    void translateToPanda() override
    {
        string msg = m_foreigner->confession();
        // 翻译并将信息传递给熊猫对象
        recvMessage("法国人说: " + msg);
    }
    void translateToHuman() override
    {
        // 接收熊猫的信息
        string msg = sendMessage();
        // 翻译并将熊猫的话转发给法国人
        m_foreigner->setResult("法国佬, " + msg);
    }
};

int main()
{
    Foreigner* human = new American;
    EnglishChopper* american = new EnglishChopper(human);
    american->translateToPanda();
    american->translateToHuman();
    delete human;
    delete american;
    cout << "============================" << endl;
    human = new French;
    FrenchChopper* french = new FrenchChopper(human);
    french->translateToPanda();
    french->translateToHuman();
    delete human;
    delete french;

    return 0;
}

上面的代码和第一个版本的代码其实是没有太大区别,如果仔细观察会发现,在适配器类中使用熊猫类中的方法的时候就无需通过熊猫类的对象来调用了,因为适配器类变成了熊猫类的子类,把这些方法继承下来了。

使用这样的模型结构,有一点需要注意:如果熊猫类有子类,那么还是建议将熊猫类和适配器类设置为关联关系。

其实关于适配器模式还有另外的一种实现方式:就是让适配器类继承它要为之提供服务器的类,也就是这个例子中的外国人类和熊猫类(如果外国人来没有子类可以使用这种方式),这种解决方案要求使用的面向对象的语言支持多继承,对于这一点C++是满足要求的,但是很多其它面向对象的语言不支持多继承。

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

在画UML类图的时候,需要具体问题具体分析,使用相同的设计模式处理不同的业务场景,绘制出的类和类之间的关系也是有些许差别的,不要死读书,读死书,头脑要活泛!

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/adapter/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


2.桥接模式【大海贼时代】

1. 组建海贼团

哥尔·D·罗杰`是罗杰海贼团船长。他最终征服了伟大航路,完成了伟大航路的航行,被人们成为`海贼王`。后来得了绝症,得知自己命不久矣,主动自首并在东海罗格镇被处刑。临死前罗杰的一句话`"想要我的宝藏吗?想要的话就全都给你!"

很多人为了梦想,为了罗杰留下的宝藏竞相出海,大海贼时代就此开启。

对于罗杰的行为,最不高兴的肯定是世界政府和海军了,世界政府是海贼中的最高权利机构,海军是世界政府的直属组织,他们以绝对的正义为名在全世界海洋执行维持治安工作。

现在海军的最高统帅也就是海军元帅是赤犬,假设现在赤犬想要将现在已知的所有的海贼团全部记录在册,那么肯定得写个程序,关于如何能够高效的记录和管理这些信息,隶属海军的程序猿们展开了讨论:

如果只记录海贼船的名字肯定的不完整的,这个海贼团内部还有一个完整的组织结构,每个海贼团有若干船员,每个船员有不同的分工,也就是他们的职责不同。

比如草帽海贼团目前一共十个人,分别是:船长剑士厨师航海士考古学家船医音乐家船工狙击手舵手

方案1

因为每个船员都具有所属海贼团的一些特性,所以可以让每个船员都从对应的海贼团类派生,如下图,它描述的某一个海贼团中类和类之间的关系:

方案2

将海贼团的船员和海贼团之间的继承关系,改为聚合关系,如下图:

盖棺定论

上面的两种方案你觉得哪个更合理一些呢?很明显,是第二种。

  • 第一种解决方案
    1. 每个船员都是当前海贼团的子类,这样船员就继承了海贼团的属性,合情合理。
    2. 如果当前海贼团添加了一个成员就需要给当前海贼团类添加一个子类。拿路飞举个例子,他在德雷斯罗萨王国打败多弗朗明哥之后,组成了草帽大船团,小弟一下子扩充了5640人,难道要给草帽团添加五千多个子类吗?如果这样处理,海贼船和船员的耦合度就比较高了。
  • 第二种解决方案
    1. 海贼团之间是继承关系,但是此时的海贼团也只是一个抽象,因为组成海贼团的人已经被抽离了,船员已经和所属的海贼团没有了继承关系。
    2. 关于海贼世界的船员在船上对应不同的职责担任不同的职务,他们是一个团队,所以可以给船员抽象出一个团队类,用于管理船上的成员。
    3. 抽象的海贼团只有一个空壳子,所以要赋予其灵魂也就是给它添加船员,此时的海贼团和船员团队可以通过聚合的方式组合成为一个整体。
    4. 这种解决方案不仅适用于管理海贼团,用于管理海军的各个舰队也是没有问题的。

通过上述分析,肯定是使用第二种解决方案程序猿的工作量会更少一些,且更容易维护和扩展。第二种方式的原则就是将抽象部分和它的实现部分分离,使它们可以独立的变化,这种处理模式就是桥接模式。

关于桥接模式的使用对应的应用场景也有很多,比如:

  1. 空调、电视机等和它们对应的遥控器
    • 空调、电视机是抽象,遥控器是实现
  2. 手机品牌和手机软件
    • 手机品牌是抽象,手机软件是实现
  3. 跨平台的GUI在不同平台上运行
    • 程序的GUI层是抽象,操作系统的API是实现

2. 路飞出海

年幼的路飞误食了红发香克斯的橡胶果实(人人果实·幻兽种·尼卡形态),受到香克斯的影响决定出海,他要做的第一件事儿就是找一艘船并找一些伙伴一起去冒险,下面我们通过桥接模式来记录下草帽团的信息。

2.1 伙伴

不论是哪艘船上的船员肯定都是有一些个人的身份信息,为了将这些信息记录下来,先定一个存储数据的结构体:

C++
// 人员信息
struct Person
{
    Person(string name, string job, string ability, string reward, string biezhu=string())
    {
        this->name = name;
        this->job = job;
        this->ability = ability;
        this->reward = reward;
        this->beiZhu = biezhu;
    }
    ~Person()
    {
        cout << name << "被析构..." << endl;
    }
    string name;    // 名字
    string job;     // 职责
    string ability; // 能力
    string reward;  // 赏金
    string beiZhu;  // 备注
};

在上面已经提到了,关于团队的成员组成可以是海贼,也可以是海军,所以可以先定义一个团队的抽象类:

C++
// 抽象船员
class AbstractTeam
{
public:
    AbstractTeam(string name) : m_teamName(name){}
    string getTeamName()
    {
        return m_teamName;
    }
    void addMember(Person* p)
    {
        m_infoMap.insert(make_pair(p->name, p));
    }
    void show()
    {
        cout << m_teamName << ": " << endl;
        for (const auto& item : m_infoMap)
        {
            cout << "【Name: " << item.second->name
                << ", Job: " << item.second->job
                << ", Ability: " << item.second->ability
                << ", MoneyReward: " << item.second->reward
                << ", BeiZhu: " << item.second->beiZhu 
                << "】" << endl;
        }
    }
    virtual void executeTask() = 0;   // 执行任务
    virtual ~AbstractTeam()
    {
        for (const auto& item : m_infoMap)
        {
            delete item.second;
        }
    }
    
protected:
    string m_teamName = string();
    map<string, Person*> m_infoMap;
};

在上面的抽象类中可以添加addMember(Person* p)和显示show()当前团队成员的信息。不同的团队他们的目标是不一样的,所以需要在子类中重写这个纯虚函数executeTask()

当路飞一行人到达东海罗格镇的时候,就开始被当时还是海军大佐的斯摩格追捕(现在是中将),接下来基于上面的基类将路飞和斯摩格团队对应的类定义出来:

路飞

C++
class CaoMaoTeam : public AbstractTeam
{
public:
    using AbstractTeam::AbstractTeam;
    void executeTask() override
    {
        cout << "在海上冒险,找到 ONE PIECE 成为海贼王!" << endl;
    }
};

在子类CaoMaoTeam中必须重写父类的纯虚函数executeTask(),这样才能创建这个子类的实例对象。

斯摩格

C++
class SmokerTeam : public AbstractTeam
{
public:
    using AbstractTeam::AbstractTeam;
    void executeTask() override
    {
        cout << "为了正义, 先将草帽一伙一网打尽!!!" << endl;
    }
};

在子类SmokerTeam中必须重写父类的纯虚函数executeTask(),这样才能创建这个子类的实例对象,斯摩格和路飞的立场不同,所以他们通过这个函数干的事情是不一样的。

2.2 海上交通工具

不论是海军还是海贼在大海上航行都需要船,虽然他们驾驶的船只不同,但是有很多属性还是一致的,所以我们可以先定义一个船的抽象类:

C++
// 船的抽象类
class AbstractShip
{
public:
    AbstractShip(AbstractTeam* team) : m_team(team) {}
    void showTeam()
    {
        m_team->show();
        m_team->executeTask();
    }
    virtual string getName() = 0;
    virtual void feature() = 0;
    virtual ~AbstractShip() {}
protected:
    AbstractTeam* m_team = nullptr;
};

在这个抽象类中提供了一个纯虚函数用来描述船的特点feature(),在不同的子类中都需要重写这个虚函数。

对于一个海贼团或者一支海军部队来说,光有船是不完整的,船只是这个团队的抽象,如果想要让它鲜活起来就必要要有由人组成的团队,也就是抽象的具体实现。所以,在这个抽象类中包含了一个团队对象,船和团队二者之间的关系可以看做是聚合关系。

梅利号

路飞来到东海的西罗布村,得到梅利号,这是草帽海贼团的第一艘船,下面把梅利号对应的类定义出来:

C++
class Merry : public AbstractShip
{
public:
    using AbstractShip::AbstractShip;
    string getName() override
    {
        return string("前进·梅利号");
    }
    void feature() override
    {
        cout << getName() 
            << " -- 船首为羊头, 在司法岛化身船精灵舍己救下了草帽一伙!" << endl;
    }
};

海军无敌战舰

C++
class HaiJunShip : public AbstractShip
{
public:
    using AbstractShip::AbstractShip;
    string getName() override
    {
        return string("无敌海军号");
    }
    void feature() override
    {
        cout << getName() << " -- 船底由海楼石建造, 可以穿过无风带的巨大炮舰!" << endl;
    }
};

2.3 你追我跑

在上面的讲解中,我们已经把要出海冒险的抽象部分(海贼船/海军军舰)和实现部分(海贼团团队/海军部队)分别定义出来了,最后需要将它们合二为一,使他们成为一个有灵魂的整体。

C++
int main()
{
    // 草帽海贼团
    CaoMaoTeam* caomao = new CaoMaoTeam("草帽海贼团");
    Person* luffy = new Person("路飞", "船长", "橡胶果实能力者", "30亿贝里", "爱吃肉");
    Person* zoro = new Person("索隆", "剑士", "三刀流", "11亿1100万贝里", "路痴");
    Person* sanji = new Person("山治", "厨师", "隐形黑", "10亿3200万贝里", "好色");
    Person* nami = new Person("娜美", "航海士", "天候棒+宙斯", "3亿6600万贝里", "喜欢钱");
    caomao->addMember(luffy);
    caomao->addMember(zoro);
    caomao->addMember(sanji);
    caomao->addMember(nami);
    Merry* sunny = new Merry(caomao);
    sunny->feature();
    sunny->showTeam();

    // 斯摩格的船队
    SmokerTeam* team = new SmokerTeam("斯摩格的海军部队");
    Person* smoker = new Person("斯摩格", "中将", "冒烟果实能力者", "", "爱吃烟熏鸡肉");
    Person* dasiqi = new Person("达斯琪", "大佐", "一刀流", "", "近视");
    team->addMember(smoker);
    team->addMember(dasiqi);
    HaiJunShip* ship = new HaiJunShip(team);
    ship->feature();
    ship->showTeam();

    delete caomao;
    delete sunny;
    delete team;
    delete ship;

    return 0;
}

程序输出的结果为:

C++
前进·梅利号 -- 船首为羊头, 在司法岛化身船精灵舍己救下了草帽一伙!
草帽海贼团:
【Name: 路飞, Job: 船长, Ability: 橡胶果实能力者, MoneyReward: 30亿贝里, BeiZhu: 爱吃肉】
【Name: 娜美, Job: 航海士, Ability: 天候棒+宙斯, MoneyReward: 3亿6600万贝里, BeiZhu: 喜欢钱】
【Name: 山治, Job: 厨师, Ability: 隐形黑, MoneyReward: 10亿3200万贝里, BeiZhu: 好色】
【Name: 索隆, Job: 剑士, Ability: 三刀流, MoneyReward: 11亿1100万贝里, BeiZhu: 路痴】
在海上冒险,找到 ONE PIECE 成为海贼王!
=====================================
无敌海军号 -- 船底由海楼石建造, 可以穿过无风带的巨大炮舰!
斯摩格的海军部队:
【Name: 达斯琪, Job: 大佐, Ability: 一刀流, MoneyReward: , BeiZhu: 近视】
【Name: 斯摩格, Job: 中将, Ability: 冒烟果实能力者, MoneyReward: , BeiZhu: 爱吃烟熏鸡肉】
为了正义, 先将草帽一伙一网打尽!!!
================== 资源释放 ==================
路飞被析构...
娜美被析构...
山治被析构...
索隆被析构...
达斯琪被析构...
斯摩格被析构...

这样,我们就通过桥接模式完成了对于海贼或者海军的管理。

3. 结构图

最后根据上面的代码将其对应的桥接模式的UML类图画出来(学会桥接模式之后,应该先画类图在写程序。)

上面已经多次提到,桥接模式就是就将抽象和实现分离开来,在上图中描述二者之间的聚合关系的那条线就可以看做是一座桥梁,把两个独立的对象关联到了一起。在某些业务场景下抽象部分或者实现部分可能是没有子类的,这在桥接模式中是被允许的。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/bridge/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


3.组合模式【草帽大船团】

1. 好大一棵树

路飞在德雷斯罗萨打败多弗朗明哥之后,一些被路飞解救的海贼团自愿加入路飞麾下,自此组成了草帽大船团,旗下有7为船长,分别是:

  1. 俊美海贼团75人
  2. 巴托俱乐部56人
  3. 八宝水军1000人
  4. 艾迪欧海贼团4人
  5. 咚塔塔海贼团200人
  6. 巨兵海贼团5人
  7. 约塔玛利亚大船团4300人

小弟数量总计5640人。

对于草帽大船团的结构组成,很像一棵树:路飞是这棵树的根节点,旗下的七个船长是路飞的子节点。在这七个船长的旗下可能还有若干个船长。。。

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

现实生活中能够和组合模式对应的场景也有很多,下面举例说明:

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

2. 大决战

在海贼中,大家都预测路飞的对手应该是同为四皇的黑胡子,黑胡子手下也有很多海贼船,双方一旦开战,必定死伤无数,最后的赢家就可以得到罗杰所留下的大秘宝ONE PIECE,并成为新的海贼王。

为了让路飞成为海贼王,我决定使用组合模式为路飞写一个管理草帽大船团的程序,其对应的主要操作是这样的:扩充船员战斗减员显示各船队信息加入战斗等。

2.1 团队管理

对于组合模式来说,操作这个集合中的任意一个节点的方式都是相同的,所以必须要先定义出单个节点的抽象,在这个抽象类中定义出节点的行为。

C++
// 抽象节点
class AbstractTeam
{
public:
    AbstractTeam(string name) :m_name(name) {}
    // 设置父节点
    void setParent(AbstractTeam* node)
    {
        m_parent = node;
    }
    AbstractTeam* getParent()
    {
        return m_parent;
    }
    string getName()
    {
        return m_name;
    }
    virtual bool hasChild()
    {
        return false;
    }
    virtual void add(AbstractTeam* node) {}
    virtual void remove(AbstractTeam* node) {}
    virtual void fight() = 0;
    virtual void display() = 0;
    virtual ~AbstractTeam() {}
protected:
    string m_name;
    AbstractTeam* m_parent = nullptr;
};

草帽大船团中有若干个番队,这个抽象类对应的就是以船为单位的一个团队(一艘船就是一个节点),它内部定义了如下方法:

  1. 设置和获得当前船队的名字
    • 设置名字:构造函数
    • 获得名字:getName()
  2. 设置和得到当前船队节点的父节点
    • 设置父节点:setParent(AbstractTeam* node)
    • 得到父节点:getParent()
  3. 给当前番队添加一个子船队节点:add(AbstractTeam* node)
  4. 跟当前番队删除一个子船队节点:remove(AbstractTeam* node)
  5. 当前番队和敌人战斗:fight()
  6. 显示当前番队的信息:display()

2.2 叶子节点

草帽大船团是一种组合模式,也就是一种树状结构,在最末端的节点就没有子节点了,这种节点可以将其称之为叶子节点。叶子节点也是一个船队,所以它肯定是需要继承抽象节点类的。

C++
// 叶子节点的小队
class LeafTeam : public AbstractTeam
{
public:
    using AbstractTeam::AbstractTeam;
    void fight() override
    {
        cout << m_parent->getName() + m_name + "与黑胡子的船员进行近距离肉搏战..." << endl;
    }
    void display() override
    {
        cout << "我是" << m_parent->getName() << "下属的" << m_name << endl;
    }
    ~LeafTeam()
    {
        cout << "我是" << m_parent->getName() << "下属的" << m_name 
            << ", 战斗已经结束, 拜拜..." << endl;
    }
};

叶子节点对应的番队由于没有子节点,所以在其对应的类中就不需要重写父类的add(AbstractTeam* node)remove(AbstractTeam* node)方法了,这也是基类中为什么不把这两个虚函数指定为纯虚函数的原因。

2.3 管理者节点

所谓的管理者节点其实就是非叶子节点。这种节点还拥有子节点,它的实现肯定是需要继承抽象节点类的。

C++

// 管理者节点
class ManagerTeam : public AbstractTeam
{
public:
    using AbstractTeam::AbstractTeam;
    void fight() override
    {
        cout << m_name + "和黑胡子的恶魔果实能力者战斗!!!" << endl;
    }
    void add(AbstractTeam* node) override
    {
        node->setParent(this);
        m_children.push_back(node);
    }
    void remove(AbstractTeam* node) override
    {
        node->setParent(nullptr);
        m_children.remove(node);
    }
    bool hasChild()
    {
        return true;
    }
    list<AbstractTeam*> getChildren()
    {
        return m_children;
    }
    void display()
    {
        string info = string();
        for (const auto item : m_children)
        {
            if (item == m_children.back())
            {
                info += item->getName();
            }
            else
            {
                // 优先级: + > +=
                info += item->getName() + ", ";
            }
        }
        cout << m_name + "的船队是【" << info << "】" << endl;
    }
    ~ManagerTeam()
    {
        cout << "我是【" << m_name << "】战斗结束, 拜拜..." << endl;
    }
private:
    list<AbstractTeam*> m_children;
};

在管理者节点类的内部有一个容器list,容器内存储的就是它的子节点对象:

  • 通过add(AbstractTeam* node)把当前番队的子节点存储到list
  • 通过remove(AbstractTeam* node)把某一个子节点从当前番队的list中删除
  • 通过display()来遍历这个list容器中的节点

2.4 战斗

最后把测试程序写一下:

C++

// 内存释放
void gameover(AbstractTeam* root)
{
    if (root == nullptr)
    {
        return;
    }
    if (root && root->hasChild())
    {
        ManagerTeam* team = dynamic_cast<ManagerTeam*>(root);
        list<AbstractTeam*> children = team->getChildren();
        for (const auto item : children)
        {
            gameover(item);
        }
    }
    delete root;
}

// 和黑胡子战斗
void fighting()
{
    vector<string> nameList = {
        "俊美海贼团", "巴托俱乐部", "八宝水军", "艾迪欧海贼团",
        "咚塔塔海贼团", "巨兵海贼团", "约塔玛利亚大船团"
    };
    // 根节点
    ManagerTeam* root = new ManagerTeam("草帽海贼团");
    for (int i = 0; i < nameList.size(); ++i)
    {
        ManagerTeam* child = new ManagerTeam(nameList.at(i));
        root->add(child);
        if (i == nameList.size() - 1)
        {
            // 给最后一个番队添加子船队
            for (int j = 0; j < 9; ++j)
            {
                LeafTeam* leaf = new LeafTeam("第" + to_string(j + 1) + "番队");
                child->add(leaf);
                leaf->fight();
                leaf->display();
            }
            child->fight();
            child->display();
        }
    }
    root->fight();
    root->display();

    cout << "====================================" << endl;
    gameover(root);
}

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

输出的结果为:

C++
约塔玛利亚大船团第1番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第1番队
约塔玛利亚大船团第2番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第2番队
约塔玛利亚大船团第3番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第3番队
约塔玛利亚大船团第4番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第4番队
约塔玛利亚大船团第5番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第5番队
约塔玛利亚大船团第6番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第6番队
约塔玛利亚大船团第7番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第7番队
约塔玛利亚大船团第8番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第8番队
约塔玛利亚大船团第9番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第9番队
约塔玛利亚大船团和黑胡子的恶魔果实能力者战斗!!!
约塔玛利亚大船团的船队是【第1番队, 第2番队, 第3番队, 第4番队, 第5番队, 第6番队, 第7番队, 第8番队, 第9番队】
草帽海贼团和黑胡子的恶魔果实能力者战斗!!!
草帽海贼团的船队是【俊美海贼团, 巴托俱乐部, 八宝水军, 艾迪欧海贼团, 咚塔塔海贼团, 巨兵海贼团, 约塔玛利亚大船团】
====================================
我是【俊美海贼团】战斗结束, 拜拜...
我是【巴托俱乐部】战斗结束, 拜拜...
我是【八宝水军】战斗结束, 拜拜...
我是【艾迪欧海贼团】战斗结束, 拜拜...
我是【咚塔塔海贼团】战斗结束, 拜拜...
我是【巨兵海贼团】战斗结束, 拜拜...
我是约塔玛利亚大船团下属的第1番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第2番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第3番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第4番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第5番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第6番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第7番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第8番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第9番队, 战斗已经结束, 拜拜...
我是【约塔玛利亚大船团】战斗结束, 拜拜...
我是【草帽海贼团】战斗结束, 拜拜...

由于草帽大船团对应的设计模式是组合模式,它对应的是一个树模型,并且每个节点的操作方式都形同,所以在释放节点的时候就可以使用递归了,gameover()函数就是一个递归函数。

3. 结构图

学完了组合模式,根据上面的例子把对应的UML类图画一下(学会之后就得先画类图,再写程序了)

为了能够更加清楚地描述出设计模式中的组合关系(不是UML中的组合关系),在AbstractTeamManagerTeam之间画了两条线:

  • 继承关系:对节点的操作使用的是抽象类中提供的接口,以保证操作的一致性

  • 聚合关系:

    ManagerTeam
    

    类型的节点还可以有子节点,父节点和子节点的之间的关系需要具体问题具体分析

    • 子节点跟随父节点一起销毁,二者就是组合关系(UML中的组合关系)
    • 子节点不跟随父节点一起销毁,二者就是聚合关系
    • 上面的程序中,在父节点的析构函数中没有销毁它管理的子节点,所以在上图中标记的是聚合关系

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/composite/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


4.装饰模式【黑胡子】

1. 马歇尔·D·蒂奇

在海贼世界中,马歇尔·D·蒂奇绰号黑胡子,他是黑胡子海贼团的提督、新世界四皇之一,自然系暗暗果实和超人系震震果实能力者,据说他还可以吃下第三个恶魔果实(就目前剧情而言尚未盖棺定论)。

对于黑胡子来说,它拥有的恶魔果实能力并不是与生俱来的,也就是后天获得,恶魔果实能力是对黑胡子原有实力的加成。黑胡子获得的这种恶魔果实能力和设计模式中的装饰模式差不多,都是动态的给一个对象绑定额外的属性(比如:能力、职责、功能等)。

关于装饰模式也可以称之为封装模式,所谓的封装就是在原有行为之上进行拓展,并不会改变该行为,看下面的例子:

  1. 在进行网络通信的时候,数据是基于IOS七层或四层网络模型(某些层合并之后就是四层模型)进行传输,通过下图可得知从应用层到物理层,数据每向下走一层就会被封装一层,最后将封装好的数据以比特流的方式发送给接收端。封装之后数据只是变得更复杂了, 并没有改变它是数据的本质。
  1. A端和B端进行网络通信,默认数据是在网络环境中裸奔的,如果想要对数据进行装饰(也就是封装)就可以在发送数据之前对其加密,接收端收到数据之后对其解密。加解密是对数据的装饰,但是没有改变数据的本质。
  2. 平时都是穿长衣长裤,疫情来袭之后穿上了防护服。防护服是对人的装饰,没有改变本体是人的本质。

上述例子中都是对原有行为进行了拓展,但是并没有改变原有行为,就好比饿了去煮面条,为了使其更美味最终会对其进行装饰,做成打卤面、炸酱面、热干面、油泼面等,不论怎么处理最终得到的还是面条,最终不可能得到一锅酱牛肉,大方向是不会变的。

2. 解构黑胡子

从概念上对装饰模式有了一定的了解之后,继续分析黑胡子的个人战斗力,根据文章开头对他的介绍,可以知道他的能力来自三个不同的方向:

  1. 与生俱来加上后天努力练就的本领
  2. 来自自然系·暗暗果实的能力
  3. 来自超人系·震震果实的能力

所以这两个恶魔果实的能力就是对黑胡子个人战力的装饰(加成)。另外,还需要明确一点,对于恶魔果实来说不是只有黑胡子能吃,谁吃了都会拥有对应的果实能力,这样这个人的战斗力也就提升了。

2.1 战魂

人自身是拥有战斗力的,而恶魔果实又可以给人附加战斗力,所以我们就可以定义一个战士的抽象类(这个抽象类不能被实例化)。

C++
// 战士的抽象类
class Soldier
{
public:
    Soldier() {}
    Soldier(string name) : m_name(name) {}
    string getName() 
    { 
        return m_name;
    };
    virtual void fight() = 0;
    virtual ~Soldier() {}
protected:
    string m_name = string();
};

有了这个抽象类就可以对某个人,或者某个恶魔果实的战力进行具体的实现,也就是说它们需要继承这个抽象类。

  • 所有的战士都可以战斗 -- fight()
  • 所有的战士都有自己的名字
    • 设置名字 -- 构造函数Soldier(string name)
    • 获取名字 -- string getName()

2.2 黑胡子

上面的战士是一个抽象类,如果想要对它进行实例化就需要写一个子类继承这个抽象类,下面我们定义一个黑胡子类:

C++
// 黑胡子(Marshall·D·Teach)
class Teach : public Soldier
{
public:
    using Soldier::Soldier;
    void fight() override
    {
        cout << m_name << "依靠惊人的力量和高超的体术战斗..." << endl;
    }
};

在黑胡子类中主要是重写了从父类继承的纯虚函数,这样黑胡子这个类就可以被实例化,得到对应的黑胡子对象了。

2.3 附魔

如果黑胡子想要让自己的实力再提升一个层次,就需要得到外部力量的辅助,可行的方案就是吃恶魔果实,这样就相当于给自己附魔了。恶魔果实的作用是对战士的战力进行装饰,使其战力得到大大的提升,所以恶魔果实类也可以先继承战士这个类。

C++
// 抽象的恶魔果实
class DevilFruit : public Soldier
{
public:
    // 指定要给哪个人吃恶魔果实 -- 附魔
    void enchantment(Soldier* soldier)
    {
        m_human = soldier;
        m_name = m_human->getName();
    }
    virtual ~DevilFruit() {}
protected:
    Soldier* m_human = nullptr;
};

上面的恶魔果实类DevilFruit继承了战士类Soldier之后还是一个抽象类,关于这个类有以下几点需要说明:

  1. DevilFruit 类中没有重写父类Soldier的纯虚函数fight(),所以它还是抽象类
  2. 恶魔果实有很多种类,每种恶魔果实能力不同,所以战斗方式也不同,因此需要在恶魔果实的子类中根据每种果实能力去重写作战函数fight()的行为。
  3. 恶魔果实DevilFruit 类的作用是给某个Soldier的子类对象附魔,所以在类内部提供了一个附魔函数enchantment(Soldier* soldier),参数就是即将要得到恶魔果实能力的那个战士。

2.4 群魔乱舞

黑胡子目前一共吃下了两颗恶魔果实:自然系暗暗果实和超人系震震果实,所以需先定义两个恶魔果实的子类:

C++
// 暗暗果实
class DarkFruit : public DevilFruit
{
public:
    void fight() override
    {
        m_human->fight();
        // 使用当前恶魔果实的能力
        cout << m_human->getName() 
            << "吃了暗暗果实, 可以拥有黑洞一样的无限吸引力..." << endl;
        warning();
    }
private:
    void warning()
    {
        cout << m_human->getName() 
            << "你要注意: 吃了暗暗果实, 身体元素化之后不能躲避攻击,会吸收所有伤害!" << endl;
    }
};

// 震震果实
class QuakeFruit : public DevilFruit
{
public:
    void fight() override
    {
        m_human->fight();
        cout << m_human->getName() 
            << "吃了震震果实, 可以在任意空间引发震动, 摧毁目标...!" << endl;
    }
};

关于这两个恶魔果实子类需要说明以下几点:

  1. 在重写父类的fight()函数的时候,用当前恶魔果实能力和战士的自身能力进行了加成,调用了战士对象的作战函数 m_human->fight(),在原有基础上提升了其战斗力。
  2. 在两个恶魔果实子类中,可以根据实际需要定义类独有的方法,比如:DarkFruit 类中有 warning() 方法,QuakeFruit 类中却没有
  3. 再次强调,这两个子类都继承了父类的附魔函数enchantment(Soldier* soldier),这样就可以完成对战士战力的加成(装饰)了。

假设黑胡子确实是可以吃下第三个恶魔果实,并且发现了一颗神奇的超人系恶魔果实大饼果实,可以将身边的一切物体变成大饼,帮助自己和队友快速回血。

C++
// 大饼果实
class PieFruit : public DevilFruit
{
public:
    void fight() override
    {
        m_human->fight();
        cout << m_human->getName()
            << "吃了大饼果实, 获得大饼铠甲...!" << endl;
        ability();
    }

    void ability()
    {
        cout << "最强辅助 -- 大饼果实可以将身边事物变成大饼, 帮助自己和队友回血..." << endl;
    }
};

使用装饰模式,可以非常方便地给任意一个战士增加战斗技能,而无需修改原有代码,完全符合开放 -- 封闭原则。

2.5 六边形战士

最后展示一下无敌的四皇之一的黑胡子的战斗力:

C++
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;
}

输出的结果如下:

C++
马歇尔·D·蒂奇依靠惊人的力量和高超的体术战斗...
马歇尔·D·蒂奇吃了暗暗果实, 可以拥有黑洞一样的无限吸引力...
马歇尔·D·蒂奇你要注意: 吃了暗暗果实, 身体元素化之后不能躲避攻击,会吸收所有伤害!
马歇尔·D·蒂奇吃了震震果实, 可以在任意空间引发震动, 摧毁目标...!
马歇尔·D·蒂奇吃了大饼果实, 获得大饼铠甲...!
最强辅助 -- 大饼果实可以将身边事物变成大饼, 帮助自己和队友回血...

关于装饰模式就是在原有基础上一层一层进行包装,对于黑胡子的能力也是如此,不论是Teach 类 还是恶魔果实类的子类DarkFruit、QuakeFruit、PieFruit它们都是Soldier 类的子类,所以新的恶魔果实对象是可以为旧的恶魔果实附魔的,因为在恶魔果实内部都绑定了一个实体,他就是黑胡子的对象,最终所有恶魔果实的能力都集中在了这个黑胡子对象身上。

C++
// 给黑胡子附魔
dark->enchantment(teach);
// 继续附魔
quake->enchantment(dark);
// 继续附魔
pie->enchantment(quake);

3. 结构图

最后根据黑胡子吃恶魔果实的这个例子把装饰模式对应的UML类图画一下(等学会了装饰模式之后,需要先画UML类图再写程序)。

恶魔果实类DevilFruit就是装饰模式中的装饰类的基类,并且恶魔果实类和父类 Soldier之间还是聚合关系,通过它的派生类DarkFruit、QuakeFruit、PieFruit最终实现了对Teach 类的装饰,使黑胡子这个主体有了三种恶魔果实能力,最终是战力fight()得到了加成效果。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/decorator/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


5.外观模式【桑尼号的狮吼炮】

1. 开炮

桑尼号是草帽一伙的第二艘海贼船,设计者是弗兰奇,使用的材料是价值不菲、世界上最强的巨树"亚当"。主要特色是狮子造形的船头及"士兵船坞系统",此外还有草皮做成的甲板,附有大型水族馆的房间、图书馆等许多方便的设备。

桑尼号船头的狮吼炮是一个非常厉害的武器。它能够从狮嘴发射出威力超强的加农炮,可利用狮头内部操控室的"狙击圈"自由网罗目标。要发动狮吼炮必须同时使用两桶可乐的能量'风来喷射'才可以避免后座力让船飞退。

想要发射狮吼炮,操作很简单:瞄准目标,拉动拉杆就可以发射了。但是看似简单的加农炮发射,其底层是需要很多个系统协同配合才能完成的:

  1. 可乐注入系统
  2. 能量转换系统,将注入的可乐转换成能量
  3. 瞄准系统
  4. 目标锁定系统
  5. 加农炮控制系统
  6. 风来炮稳定系统,抵消后坐力,让船体稳定。

这么复杂的系统对于使用者来说其实就是一个按钮加一个拉杆。不论狮吼炮的设计者弗兰奇有没有学过设计模式,但他确实用到了一种设计模式:外观模式。外观模式就是给很多复杂的子系统提供一个简单的上层接口,并在这些接口中包含用户真正关心的功能。

关于外观模式的应用,在实际生活中也有有很多的场景:

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

2. 庖丁解牛

2.1 UML类图

关于桑尼号的狮吼炮的组成前边已经描述过了,我们需要通过外观模式对其进行封装,如果仔细分析一下可以得知,上层的接口和底层的各个模块之间应该是关联关系(因为类之间没有继承关系,也不是整体和部分这种结构,因此排除了聚合和组合,并且它们之间具有包含和被包含的关系,所以确定的关系是关联关系),下面是狮吼炮对应的UML类图:

2.2 子系统

根据上面提供的类图就可以把对应的程序写出来了:

C++

// 乐可系统
class CokeSystem
{
public:
    void immitCoke()
    {
        cout << "狮吼炮原料<可乐>已经注入完毕..." << endl;
    }
};

// 能量转换系统
class EnergySystem
{
public:
    void energyConvert()
    {
        cout << "已经将所有的可乐转换为了能量..." << endl;
    }
};

// 目标锁定系统
class AimLockSystem
{
public:
    void aimLock()
    {
        cout << "已经瞄准并且锁定了目标..." << endl;
    }
};

// 加农炮发射系统
class Cannon
{
public:
    void cannonFire()
    {
        cout << "狮吼炮正在向目标开火..." << endl;
    }
};

// 风来炮稳定系统
class WindCannon
{
public:
    void windCannonFire()
    {
        cout << "发射风来炮抵消后坐力稳定船身..." << endl;
    }
};

这些子系统都是可以独立工作的,并且都提供了供外部调用的接口。

2.3 狮吼炮

狮吼炮是上层接口,需要协调上面的这些子系统使它们能够相互配合协同工作。

C++
// 上层接口
class LionCannon
{
public:
    LionCannon()
    {
        m_coke = new CokeSystem;
        m_energy = new EnergySystem;
        m_aimLock = new AimLockSystem;
        m_cannon = new Cannon;
        m_windCannon = new WindCannon;
    }
    ~LionCannon()
    {
        delete m_coke;
        delete m_energy;
        delete m_aimLock;
        delete m_cannon;
        delete m_windCannon;
    }
    // 瞄准并锁定目标
    void aimAndLock()
    {
        m_coke->immitCoke();
        m_energy->energyConvert();
        m_aimLock->aimLock();
    }
    // 开炮
    void fire()
    {
        m_cannon->cannonFire();
        m_windCannon->windCannonFire();
    }
private:
    CokeSystem* m_coke = nullptr;
    EnergySystem* m_energy = nullptr;
    AimLockSystem* m_aimLock = nullptr;
    Cannon* m_cannon = nullptr;
    WindCannon* m_windCannon = nullptr;
};

在狮吼炮上层接口类中只提供了两个方法:瞄准锁定 aimAndLock()开火 fire()。这样对于狮吼炮的操作难度瞬间变成了傻瓜级,只要有手并且眼睛不瞎就可以操作。

2.4 开炮

最后需要展示一下狮吼炮的威力:

C++
int main()
{
    // 发射狮吼炮
    LionCannon* lion = new LionCannon;
    lion->aimAndLock();
    lion->fire();
    delete lion;
    return 0;
}

输出的结果为:

C++
狮吼炮原料<可乐>已经注入完毕...
已经将所有的可乐转换为了能量...
已经瞄准并且锁定了目标...
狮吼炮正在向目标开火...
发射风来炮抵消后坐力稳定船身...

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/facade/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


6.享元模式【铁拳卡普】

1. 拳骨陨石

蒙奇·D·卡普,也被称为"铁拳卡普",是海军中的传奇人物,相传卡普数次将海贼王罗杰逼入绝境,被誉为"海军英雄"。卡普是路飞的爷爷,经常将霸气缠绕在拳头上来打艾斯与路飞的头,因为攻击中充满了爱,所以名为"爱之铁拳",每次都会在路飞头上留下包。

虽然自己是海军,但是儿子却是革命军,孙子是海贼。拳骨陨石·流星群是卡普的战斗招式之一,即连续不断地向敌人投掷炮弹,其效果就如流星砸向敌人一般。卡普曾以此招在水之七都向路飞告别,我们来回顾一下当时的场景:

关于卡普老爷子的实力是毋庸置疑的,假设我们现在是负责游戏开发的程序猿,要复刻这段场景,其中出现频率最高的就是炮弹。这里有一个很现实的亟待解决的问题:内存的消耗问题

  • 每个炮弹都是一个对象,每个对象都会占用一块内存
  • 炮弹越多,占用的内存就越大,如果炮弹足够多可能会出现内存枯竭问题
  • 假设内存足够大,频繁的创建炮弹对象,会影响游戏的流畅度,性能低

关于游戏中的炮弹,应该有以下一些需要处理的属性:

  1. 炮弹的坐标
  2. 炮弹的速度
  3. 炮弹的颜色渲染
  4. 炮弹的精灵图(就是一张大图上有很多小的图片,通过进行位置的控制,从大图中取出想要的某一张小的图片

上面就是一张关于炮弹爆炸过程的精灵图片,假设一张炮弹的精灵图片有200k,那么1000个这样的炮弹占用的内存就是200M,这对于内存的消耗是非常惊人的。

游戏中制作一个炮弹需要的数据在上面已经分析出来了,在这四部分数据中有些属性是动态的,有些属性是静态的:

  • 静态资源:精灵图渲染的颜色
  • 动态属性:坐标速度

对应的动态资源肯定是不能被复用,所有炮弹可共享的就是这些静态资源,不论有多少炮弹,它们对应的精灵图渲染颜色数据可以只有一份,这样对于内存的开销就大大降低了。

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

在上图跷跷板的左侧每个对象使用的都是独立内存,而右侧所有对象都共同使用了某一部分内存,所以左侧重右侧轻,左侧占用内存多,右侧占用内存少。

享元模式和线程池比较类似,线程池可以复用线程,有效避免了线程的频繁创建和销毁,减少了性能的消耗并提高了工作效率。享元模式中的共享内存也可以将其称之为缓存,这种模式中共享的是对象。

对象的常量数据通常被称为内在状态, 其位于对象中, 其他对象只能读取但不能修改其数值。 而对象的其他状态常常能被其他对象 "从外部" 改变, 因此被称为外在状态。使用享元模式一般建议将内在状态和外在状态分离,将内在状态单独放到一个类中,这种类我们可以将其称之为享元类。

2. 设计炮弹

2.1 炸弹弹体

炮弹的共享数据其实就是享元模式中的享元数据,先定义一个共享数据类:

C++
// 共享数据类
class SharedBombBody
{
public:
    SharedBombBody(string sprite) : m_sprite(sprite) 
    {
        cout << "正在创建 <" << m_sprite << ">..." << endl;
    }
    void move(int x, int y, int speed)
    {
        cout << "炸弹以每小时" << speed << "速度飞到了(" 
            << x << ", " << y << ") 的位置..." << endl;
    }
    void draw(int x, int y)
    {
        cout << "在 (" << x << ", " << y << ") 的位置重绘炸弹弹体..." << endl;
    }

private:
    string m_sprite;    // 精灵图片
    string m_color = string("black");     // 渲染颜色
};

通过构造函数得到精灵图片之后,该类对象中的数据就不会再发生任何变化了。

2.2 炸弹

有了炸弹的弹体,卡普就可以基于这部分静态资源扔一枚炮弹出去了,先定义一个发射炸弹的类:

C++
// 发射炮弹
class LaunchBomb
{
public:
    LaunchBomb(SharedBombBody* body) : m_bomb(body) {}
    int getX()
    {
        return m_x;
    }
    int getY()
    {
        return m_y;
    }
    void setSpeed(int speed)
    {
        m_speed = speed;
    }
    int getSpeed()
    {
        return m_speed;
    }
    void move(int x, int y)
    {
        m_x = x;
        m_y = y;
        m_bomb->move(m_x, m_y, m_speed);
        draw();
    }
    void draw()
    {
        m_bomb->draw(m_x, m_y);
    }

private:
    int m_x = 0;
    int m_y = 0;
    int m_speed = 100;
    SharedBombBody* m_bomb = nullptr;
};

由于发射出的每一枚型号相同的炮弹它们的外形都是相同的,所以这些炸弹可以共享同一个弹体对象(在类内部没有创建SharedBombBody 类对象)。对于炸弹被发射出去之后它的坐标以及速度肯定是会变化的,所以在上面的LaunchBomb 类中添加了对应的getset方法。

2.3 彩蛋

在很多游戏中,由于玩家触发了某个条件,此时系统会赠送给玩家一个彩蛋,这个彩蛋一般都是独一无二的。假设卡普在投掷炸弹的过程中,路飞通过自己的橡胶果实能力连续接住了10个炸弹,并将其反弹出去,这个时候卡普投出的某一个炸弹就会变成一个彩蛋(卡普最后扔出的超巨型炸弹也可以视为是一个彩蛋),对于彩蛋的处理我们的分析如下:

  • 这个彩蛋拥有和炸弹不一样的外观(使用的精灵图不同)
  • 不论是炸弹还是彩蛋,对于卡普来说对它们的处理动作是一样的
  • 炸弹爆炸会造成伤害,彩蛋爆炸会给玩家提供奖励或者造成非常严重的伤害或开启一段支线剧情等

通过上述的三点分析,我们可以得出结论,彩蛋和炸弹有相同的处理动作,只不过在细节处理上略有不同,对于这种情况,我们一般会提供一个抽象的基类并在这个类中提供一套虚操作函数,这样在子类中就可以重写父类提供的虚函数,提供不同的处理动作了。

C++
// 享元基类
class FlyweightBody
{
public:
    FlyweightBody(string sprite) : m_sprite(sprite) {}
    virtual void move(int x, int y, int speed) = 0;
    virtual void draw(int x, int y) = 0;
    virtual ~FlyweightBody() {}
protected:
    string m_sprite;    // 精灵图片
    string m_color = string("black");     // 渲染颜色
};

// 炸弹弹体
class SharedBombBody : public FlyweightBody
{
public:
    using FlyweightBody::FlyweightBody;
    void move(int x, int y, int speed) override
    {
        cout << "炸弹以每小时" << speed << "速度飞到了(" 
            << x << ", " << y << ") 的位置..." << endl;
    }
    void draw(int x, int y) override
    {
        cout << "在 (" << x << ", " << y << ") 的位置重绘炸弹弹体..." << endl;
    }
};

// 唯一的炸弹彩蛋
class UniqueBomb : public FlyweightBody
{
public:
    using FlyweightBody::FlyweightBody;
    void move(int x, int y, int speed) override
    {
        // 此处省略对参数 x, y, speed的处理
        cout << "彩蛋在往指定位置移动, 准备爆炸发放奖励..." << endl;
    }
    void draw(int x, int y) override
    {
        cout << "在 (" << x << ", " << y << ") 的位置重绘彩蛋运动轨迹..." << endl;
    }
};

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

2.4 享元工厂

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

CPP
// 享元工厂类
class BombBodyFactory
{
public:
    SharedBombBody* getSharedData(string name)
    {
        SharedBombBody* data = nullptr;
        // 遍历容器
        for (auto item : m_bodyMap)
        {
            if (item.first == name)
            {
                // 找到了
                data = item.second;
                cout << "正在复用 <" << name << ">..." << endl;
                break;
            }
        }
        if (data == nullptr)
        {
            data = new SharedBombBody(name);
            cout << "正在创建 <" << name << ">..." << endl;
            m_bodyMap.insert(make_pair(name, data));
        }
        return data;
    }
    ~BombBodyFactory()
    {
        for (auto item : m_bodyMap)
        {
            delete item.second;
        }
    }
private:
    map<string, SharedBombBody*> m_bodyMap;
};

在享元工厂内部有一个map 容器,用于存储各种型号的炮弹的享元数据,这个享元工厂就相当于一个对象池,当调用了getSharedData(string name)函数之后,如果能够从map 容器找到name对应的享元对象就返回该对象,如果找不到就创建一个新的享元对象并储存起来,这样就可以实现对象的复用了。

2.5 发射炮弹

最后就可以把炮弹制作出来并让其在游戏中按照指定的轨迹运动了:

C++
int main()
{
    // 发射炮弹
    BombBodyFactory* factory = new BombBodyFactory;
    vector<LaunchBomb*> ballList;
    vector<string> namelist = { "撒旦-1", "撒旦-1", "撒旦-2", "撒旦-2", "撒旦-2", "撒旦-3"};
    for (auto name : namelist)
    {
        int x = 0, y = 0;
        LaunchBomb* ball = new LaunchBomb(factory->getSharedData(name));
        for (int i = 0; i < 3; ++i)
        {
            x += rand() % 100;
            y += rand() % 50;
            ball->move(x, y);
        }
        cout << "=========================" << endl;
        ballList.push_back(ball);
    }
    // 彩蛋
    UniqueBomb* unique = new UniqueBomb("大彩蛋");
    LaunchBomb* bomb = new LaunchBomb(unique);
    int x = 0, y = 0;
    for (int i = 0; i < 3; ++i)
    {
        x += rand() % 100;
        y += rand() % 50;
        bomb->move(x, y);
    }

    for (auto ball : ballList)
    {
        delete ball;
    }
    delete factory;
    delete unique;
    delete bomb;
    return 0;
}

上面的测试程序就相当于在游戏中,卡普扔出了6个炸弹和一个彩蛋,不论是炸弹还是彩蛋都可以通过LaunchBomb 类进行处理,这个类的构造函数在接收实参的时候实际上就是一个多态的应用。

3. 完整代码

添加了想元类基类之后,再次对相关的类进行修改,完整的代码如下:

C++

// 享元基类
class FlyweightBody
{
public:
    FlyweightBody(string sprite) : m_sprite(sprite) {}
    virtual void move(int x, int y, int speed) = 0;
    virtual void draw(int x, int y) = 0;
    virtual ~FlyweightBody() {}
protected:
    string m_sprite;    // 精灵图片
    string m_color = string("black");     // 渲染颜色
};

// 炸弹弹体
class SharedBombBody : public FlyweightBody
{
public:
    using FlyweightBody::FlyweightBody;
    void move(int x, int y, int speed) override
    {
        cout << "炸弹以每小时" << speed << "速度飞到了(" 
            << x << ", " << y << ") 的位置..." << endl;
    }
    void draw(int x, int y) override
    {
        cout << "在 (" << x << ", " << y << ") 的位置重绘炸弹弹体..." << endl;
    }
};

// 唯一的炸弹彩蛋
class UniqueBomb : public FlyweightBody
{
public:
    using FlyweightBody::FlyweightBody;
    void move(int x, int y, int speed) override
    {
        // 此处省略对参数 x, y, speed的处理
        cout << "彩蛋在往指定位置移动, 准备爆炸发放奖励..." << endl;
    }
    void draw(int x, int y) override
    {
        cout << "在 (" << x << ", " << y << ") 的位置重绘彩蛋运动轨迹..." << endl;
    }
};

// 发射炮弹
class LaunchBomb
{
public:
    LaunchBomb(FlyweightBody* body) : m_bomb(body) {}
    int getX()
    {
        return m_x;
    }
    int getY()
    {
        return m_y;
    }
    void setSpeed(int speed)
    {
        m_speed = speed;
    }
    int getSpeed()
    {
        return m_speed;
    }
    void move(int x, int y)
    {
        m_x = x;
        m_y = y;
        m_bomb->move(m_x, m_y, m_speed);
        draw();
    }
    void draw()
    {
        m_bomb->draw(m_x, m_y);
    }

private:
    int m_x = 0;
    int m_y = 0;
    int m_speed = 100;
    FlyweightBody* m_bomb = nullptr;
};


// 享元工厂类
class BombBodyFactory
{
public:
    SharedBombBody* getSharedData(string name)
    {
        SharedBombBody* data = nullptr;
        // 遍历容器
        for (auto item : m_bodyMap)
        {
            if (item.first == name)
            {
                // 找到了
                data = item.second;
                cout << "正在复用 <" << name << ">..." << endl;
                break;
            }
        }
        if (data == nullptr)
        {
            data = new SharedBombBody(name);
            cout << "正在创建 <" << name << ">..." << endl;
            m_bodyMap.insert(make_pair(name, data));
        }
        return data;
    }
    ~BombBodyFactory()
    {
        for (auto item : m_bodyMap)
        {
            delete item.second;
        }
    }
private:
    map<string, SharedBombBody*> m_bodyMap;
};

int main()
{
    // 发射炮弹
    BombBodyFactory* factory = new BombBodyFactory;
    vector<LaunchBomb*> ballList;
    vector<string> namelist = { "撒旦-1", "撒旦-1", "撒旦-2", "撒旦-2", "撒旦-2", "撒旦-3"};
    for (auto name : namelist)
    {
        int x = 0, y = 0;
        LaunchBomb* ball = new LaunchBomb(factory->getSharedData(name));
        for (int i = 0; i < 3; ++i)
        {
            x += rand() % 100;
            y += rand() % 50;
            ball->move(x, y);
        }
        cout << "=========================" << endl;
        ballList.push_back(ball);
    }
    // 彩蛋
    UniqueBomb* unique = new UniqueBomb("大彩蛋");
    LaunchBomb* bomb = new LaunchBomb(unique);
    int x = 0, y = 0;
    for (int i = 0; i < 3; ++i)
    {
        x += rand() % 100;
        y += rand() % 50;
        bomb->move(x, y);
    }

    for (auto ball : ballList)
    {
        delete ball;
    }
    delete factory;
    delete unique;
    delete bomb;
    return 0;
}

程序的输出的结果:

C++

正在创建 <撒旦-1>...
炸弹以每小时100速度飞到了(41, 17) 的位置...
在 (41, 17) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(75, 17) 的位置...
在 (75, 17) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(144, 41) 的位置...
在 (144, 41) 的位置重绘炸弹弹体...
=========================
正在复用 <撒旦-1>...
炸弹以每小时100速度飞到了(78, 8) 的位置...
在 (78, 8) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(140, 22) 的位置...
在 (140, 22) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(145, 67) 的位置...
在 (145, 67) 的位置重绘炸弹弹体...
=========================
正在创建 <撒旦-2>...
炸弹以每小时100速度飞到了(81, 27) 的位置...
在 (81, 27) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(142, 68) 的位置...
在 (142, 68) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(237, 110) 的位置...
在 (237, 110) 的位置重绘炸弹弹体...
=========================
正在复用 <撒旦-2>...
炸弹以每小时100速度飞到了(27, 36) 的位置...
在 (27, 36) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(118, 40) 的位置...
在 (118, 40) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(120, 43) 的位置...
在 (120, 43) 的位置重绘炸弹弹体...
=========================
正在复用 <撒旦-2>...
炸弹以每小时100速度飞到了(92, 32) 的位置...
在 (92, 32) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(113, 48) 的位置...
在 (113, 48) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(131, 93) 的位置...
在 (131, 93) 的位置重绘炸弹弹体...
=========================
正在创建 <撒旦-3>...
炸弹以每小时100速度飞到了(47, 26) 的位置...
在 (47, 26) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(118, 64) 的位置...
在 (118, 64) 的位置重绘炸弹弹体...
炸弹以每小时100速度飞到了(187, 76) 的位置...
在 (187, 76) 的位置重绘炸弹弹体...
=========================
彩蛋在往指定位置移动, 准备爆炸发放奖励...
在 (67, 49) 的位置重绘彩蛋运动轨迹...
彩蛋在往指定位置移动, 准备爆炸发放奖励...
在 (102, 93) 的位置重绘彩蛋运动轨迹...
彩蛋在往指定位置移动, 准备爆炸发放奖励...
在 (105, 104) 的位置重绘彩蛋运动轨迹...

4. 结构图

最后根据上面的代码就可以画出享元模式的UML类图了(学会了享元模式之后,应该先画UML类图,然后根据类图写代码。)

关于投掷炸弹可能也对应一个类,在上面的测试程序中对应的就是main()函数,在这个UML类图中并没有画出投掷炸弹的这个类。除此之后还有几个知识点需要我们做到心中有数:

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/flyweight/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


7.代理模式【电话虫】

1. 电话虫

在海贼中,有一种神奇的通信工具叫做电话虫(Den Den Mushi ),外形如蜗牛,身上带有斑点或条纹或通体纯色,壳顶上有对讲机或按键,不接通时会睡觉,接通时会惊醒,并发出"波噜波噜"的声音,在通话时电话虫的嘴巴会如同讲话人的嘴巴一样动,也有人的感情听得懂人类说话,工作原理是将人的声音转化为电话虫的声音进行长距离传接,经作者尾田荣一郎在SBS上证实这是自然生长的一种虫。

如果拥有了属于对方的电话虫,不论彼此相隔有多远都可以进行时时通信,通过电话虫除了可以听到对方的声音,还能看到对方的表情,妥妥的一个代理人。在设计模式中有一种模式叫做代理模式,代理模式和电话虫差不多,都是为其他对象提供一种代理,以控制对这个对象的访问。

生活中关于代理的例子也有很多,比如:

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

2. 解构电话虫

如果我们想要用代理模式来描述一下电话虫的行为,里边有如下几个细节:

  1. 说话的人是一个对象,电话虫也是一个对象,电话虫模拟的是说话的人
  2. 说话的人和电话虫有相同的行为,所以需要为二者提供一个抽象类
  3. 电话虫是在为说话的人办事,所以电话虫和说话人应该有关联关系。

根据上面的描述,先把对应的UML类图画一下:

由于电话虫类和讲话者类不是部分与整体的关系,所以这二者的关系是关联关系。

3. 通话

根据上面的UML类图,先把通话的抽象类定义出来:

C++
// 抽象通信类
class Communication
{
public:
    virtual void communicate() = 0; // 通话
    virtual ~Communication() {}
};

然后在根据这个抽象类,派生出两个子类:讲话者类和电话虫类:

C++
// 讲话的人
class Speaker : public Communication
{
public:
    void communicate() override
    {
        cout << "开始说话..." << endl;
        cout << "通话时发生了一些列的表情变化..." << endl;
    }
};

// 电话虫
class DenDenMushi : public Communication
{
public:
    DenDenMushi()
    {
        m_isStart = true;
        m_speaker = new Speaker;
    }
    ~DenDenMushi()
    {
        if (m_speaker != nullptr)
        {
            delete m_speaker;
        }
    }
    // 判断是否已经开始通话了
    bool isStart()
    {
        return m_isStart;
    }
    void communicate() override
    {
        if (isStart())
        {
            // 得到通话者语言和表情信息, 并加以模仿
            cout << "电话虫开始实时模仿通话者的语言和表情..." << endl;
            m_speaker->communicate();
       }
    }
private:
    bool m_isStart = false;
    Speaker* m_speaker = nullptr;
};

海贼官方给出的电话虫的名字叫做DenDenMushi,所以电话虫类也以此命名。

在代理类也就是电话虫类中,一般都会判断是否允许代理(对应示例程序中的isStart(),表示通话对否开始了),如果允许则通过被代理的对象m_speaker,调用它的操作函数communicate()

最后是测试代码:

C++
int main()
{
    // 直接交流
    Communication* comm = new Speaker;
    comm->communicate();
    delete comm;
    cout << "===================================" << endl;
    // 使用电话虫
    comm = new DenDenMushi;
    comm->communicate();
    delete comm;

    return 0;
}

上面的测试程序中一共使用了两种方式进行通信,第二种使用的是代理模式,我们可以在代理类中有效的管理被代理的对象的工作的时机,但是并没有改变被代理的对象的行为。

通过测试程序我们可以得到如下结论:如果使用代理模式,不能改变所代理的类的接口,使用代理模式的目的是为了加强控制。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/proxy/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


4.行为模式

1.责任链模式【巴洛克工作室】

1. 犯罪公司

在海贼世界中,巴洛克工作社是驻扎于阿拉巴斯坦的秘密犯罪公司,社长是王下七武海之一的沙·克洛克达尔。巴洛克工作社的名义上的目的是推翻阿拉巴斯坦建立理想王国,真正目的是得到古代兵器"冥王"从而获得更强大的力量。先来看一下这个公司的组织结构:

虽然巴洛克工作社是个犯罪公司,但是其内部的组织结构和分工是非常明确的。假设大聪明是这个组织中最卑微的百万长者,那么有任何需求都需要向上级汇报等待审批,比如请假、涨工资、离职等,对于不同的请求,各个层级的领导也有不同的审批权限:

  • 请假:直接上级有权进行处理
  • 涨工资:只有副社长和社长有权利进行处理
  • 离职:只有社长有权利进行处理

也就是说对于某个员工的请求可能需要一级一级向上传递,如果有权处理那就将其处理掉,如果无权处理还需继续向上传递该请求。像上面这种将对象连成一条链,并沿着这条链传递请求,直到链上有一个对象将请求处理掉为止,这种处理数据的模式叫做责任链模式。使用这种模式有一个好处:处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。

责任链 会将特定行为转换为被称作处理者的独立对象。 在巴洛克工作社这个例子中, 每个审批步骤都可被抽取为仅有单个方法的类, 并执行审批操作,请求及其数据则会被作为参数传递给该方法。

关于责任链这种模式,在日常生活中也比较常见,比如:

  • 公司的OA审批系统

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

  • GUI操作界面

2. 浪子回头

大聪明是巴洛克工作社最底层的一个小坏蛋,因为总干坏事儿受到了良心的谴责,所以要请假,被顶头上司批准,随后提出涨工资,被罗宾(副社长)拒绝,最后提出离职被克洛克达尔(社长)拒绝。下面作为程序猿的我基于责任链模式写一段代码来描述一下大聪明的遭遇。

2.1 执剑人

在责任链模式中,每个节点都有相同的处理动作(处理函数),只不过因为每个节点的权限、地位不同,(在处理函数内部)这个请求可能被处理了,也可能没有被处理,所以这些节点应该有一个共同的基类,也就是一个抽象的管理者节点类:

C++
enum class RequestType:char {QingJia, ZhangXin, CiZhi};
// 抽象的任务节点类
class AbstractManager
{
public:
    void setNext(AbstractManager* manager)
    {
        m_next = manager;
    }
    virtual void handleRequest(RequestType type) = 0;
    virtual ~AbstractManager() {}
protected:
    AbstractManager* m_next = nullptr;
};

在这个抽象类中提供了一个setNext()方法,这样就可以将各个节点连接起来了,并且提供了一个处理请求的纯虚函数handleRequest(),关于请求的类型被定义到了枚举RequestType中。

在巴洛克工作社中不同等级的管理者权利也不同,下面需要根据管理者的权限将处理任务的函数实现出来:

C++

// 初级管理者
class Manager : public AbstractManager
{
public:
    void handleRequest(RequestType type)
    {
        switch (type)
        {
        case RequestType::QingJia:
            cout << "请假: 同意请假,好好休息~~~" << endl;
            break;
        case RequestType::ZhangXin:
            cout << "涨薪: 这个我得请示一下咱们CEO..." << " ====> ";
            m_next->handleRequest(type);
            break;
        case RequestType::CiZhi:
            cout << "辞职: 我给你向上级反应一下..." << " ====> ";
            m_next->handleRequest(type);
            break;
        default:
            break;
        }
    }
};

// CEO
class CEO : public AbstractManager
{
public:
    void handleRequest(RequestType type)
    {
        switch (type)
        {
        case RequestType::QingJia:
            cout << "请假: 同意请假, 下不为例..." << endl;
            break;
        case RequestType::ZhangXin:
            cout << "涨薪: 你工资不少了, 给你个购物券吧..." << endl;
            break;
        case RequestType::CiZhi:
            cout << "辞职: 这个我得问问咱们老板..." << " ====> ";
            m_next->handleRequest(type);
            break;
        default:
            break;
        }
    }
};

// 老板
class Boss : public AbstractManager
{
public:
    void handleRequest(RequestType type)
    {
        switch (type)
        {
        case RequestType::QingJia:
            cout << "请假: 只有工作才能实现人生价值,回去好好坚守岗位!!!" << endl;
            break;
        case RequestType::ZhangXin:
            cout << "涨薪: 钱财乃身外之物, 要视其如粪土!!!" << endl;
            break;
        case RequestType::CiZhi:
            cout << "辞职: 巴洛克工作社就是你的家, 这次把你留下, 下次别再提了!!!" << endl;
            break;
        default:
            break;
        }
    }
};

在上面三个子类中根据各自权限分别重写了handleRequest()函数。

2.2 大聪明

大聪明作为巴洛克工作社中一个身份卑微的小弟,也对应一个类,通过这个类中提供的函数就可以向领导提出请求了:

C++
// 卑微的大聪明
class DaCongMing
{
public:
    void request(RequestType type, AbstractManager* manager)
    {
        manager->handleRequest(type);
    }
};

2.3 宿命

下面是不甘心的大聪明分别向三个不同的领导提出请求的经过:

C++
int main()
{
    Manager* manager = new Manager;
    CEO* ceo = new CEO;
    Boss* boss = new Boss;
    // 设置关联关系
    manager->setNext(ceo);
    ceo->setNext(boss);

    // 卑微的大聪明的请求
    DaCongMing* boy = new DaCongMing;
    cout << "========== 大聪明向顶头上司提要求 ==========" << endl;
    boy->request(RequestType::QingJia, manager);
    boy->request(RequestType::ZhangXin, manager);
    boy->request(RequestType::CiZhi, manager);
    cout << "========== 大聪明越级找CEO提要求 ==========" << endl;
    boy->request(RequestType::QingJia, ceo);
    boy->request(RequestType::ZhangXin, ceo);
    boy->request(RequestType::CiZhi, ceo);
    cout << "========== 大聪明直接找BOSS提要求 ==========" << endl;
    boy->request(RequestType::QingJia, boss);
    boy->request(RequestType::ZhangXin, boss);
    boy->request(RequestType::CiZhi, boss);

    delete boy;
    delete manager;
    delete ceo;
    delete boss;

    return 0;
}

大聪明到的答复如下:

C++
========== 大聪明向顶头上司提要求 ==========
请假: 同意请假,好好休息~~~
涨薪: 这个我得请示一下咱们CEO... ====> 涨薪: 你工资不少了, 给你个购物券吧...
辞职: 我给你向上级反应一下... ====> 辞职: 这个我得问问咱们老板... ====> 辞职: 巴洛克工作社就是你的家, 这次把你留下, 下次别再提了!!!
========== 大聪明越级找CEO提要求 ==========
请假: 同意请假, 下不为例...
涨薪: 你工资不少了, 给你个购物券吧...
辞职: 这个我得问问咱们老板... ====> 辞职: 巴洛克工作社就是你的家, 这次把你留下, 下次别再提了!!!
========== 大聪明直接找BOSS提要求 ==========
请假: 只有工作才能实现人生价值,回去好好坚守岗位!!!
涨薪: 钱财乃身外之物, 要视其如粪土!!!
辞职: 巴洛克工作社就是你的家, 这次把你留下, 下次别再提了!!!

大聪明直接哭晕在厕所!在真实的业务场景中,不同级别的管理者在处理请求的时候会进行更加细致的判定,在当前的场景中就忽略了。

关键点:在处理请求之前必须先要把各个管理者对象按照等级关系串联起来:

C++
Manager* manager = new Manager;
CEO* ceo = new CEO;
Boss* boss = new Boss;
// 设置关联关系
manager->setNext(ceo);
ceo->setNext(boss);

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

3. 结构图

最后将上面的例子对应的UML类图画一下(学会了责任链模式之后,需要先画UML类图,再写程序。)

在上图中,通过自关联的方式,并且基于多态把当前管理者对象的下一级对象(AbstractManager 的子类对象)保存到m_next中了,这样就可以得到一个单向链,它其实还可以有其他的变种:

  • 双向链:将当前节点的后继对象和前驱对象都记录下来
  • 树状链:当前节点可以有多个子节点,也就是多个后继节点,可以将它们保存到一个STL容器中。

总之,举一反三,根据实际情况随机应变就对了。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/chain/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


2.命令模式【海上餐厅巴拉蒂】

1. 海上餐厅

在海贼世界中,巴拉蒂是位于东海桑巴斯海域的一个海上餐厅。外形是条巨型的船,船两头有鱼形状的船首,整艘船能转换成战斗状态。巴拉蒂是东海最有名的餐厅,有不少人特地为了品尝老板兼主厨哲普所做的美味料理而来到这里,甚至连海军的重要角色都会来这里吃饭。

当出身东海的路飞路过巴拉蒂餐厅的时候,由于损坏了餐厅的财物,被强行扣留下来打工以此偿还他造成的损失,替自己赎身。这天来了几个人吃饭,路飞作为服务员接待他们点餐。意料之中,他又把事儿搞砸了,下面作为程序猿的我打算替路飞写一个点餐的小程序,先来分析需求:

  1. 允许顾客点多个菜,点餐完毕后,厨师才开始制作
  2. 点餐过程中需要及时提醒顾客,这个菜现在是不是可以制作(可能原材料用完了)
  3. 需要有点餐记录,结账的时候用
  4. 顾客可以取消已下单但是还没有制作的菜

如果想要实现上述的需求,需要在程序中准备如下几个对象:

  1. 替顾客下单的服务员路飞
  2. 给顾客炒菜的厨师哲普
  3. 由路飞写好的顾客点餐列表

我们可以将顾客的点餐列表看作是一个待执行的命令的列表,这样就可以总结出三者之间的关系了:厨师哲普是这些命令的接收者和执行者,路飞是这些命令的调用者。如果没有这张点餐列表,路飞需要非常频繁地穿梭在餐厅与厨房之间,而且哪个顾客点了什么菜也容易弄混,从某种程度上讲这个点餐列表就相当于一个任务队列。

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

2. 慕名而来

2.1 厨师哲普

哲普作为一个厨师可能会制作很多的美食,在定义哲普对应的类的时候,每道菜都应该对应一个处理函数,所以这个类应该是这样的:

C++
// 厨师哲普
class CookerZeff
{
public:
    void makeDSX()
    {
        cout << "开始烹饪地三鲜...";
    }
    void makeGBJD()
    {
        cout << "开始烹饪宫保鸡丁...";
    }
    void makeYXRS()
    {
        cout << "开始烹饪鱼香肉丝...";
    }
    void makeHSPG()
    {
        cout << "开始烹饪红烧排骨...";
    }
};

这个厨师类是命令模式中命令的接收者,收不到命令厨师是不能工作的。

2.2 下单

在哲普制作美食之前,需要顾客先下单,在命令模式中顾客每点一道美食对应的就是一个命令,虽然每次点的食物不同,但点餐这个动作是不变的。因此我们可以先定义一个关于点餐命令的基类:

C++
// 点餐的命令 - 抽象类
class AbstractCommand
{
public:
    AbstractCommand(CookerZeff* receiver) : m_cooker(receiver) {}
    virtual void excute() = 0;
    virtual string name() = 0;
    ~AbstractCommand() {}
protected:
    CookerZeff* m_cooker = nullptr;
};

在这个抽象类中关联了一个厨师对象CookerZeff* m_cooker,有了这个厨师对象就可以去执行对应的炒菜的动作了excute()。基于这个抽象的基类就可以派生出若干子类,在子类中让厨师去炒菜,也就是重写excute()

C++

// 地三鲜的命令
class DSXCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeDSX();
    }
    string name() override
    {
        return "地三鲜";
    }
};

// 宫保鸡丁的命令
class GBJDCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeGBJD();
    }
    string name() override
    {
        return "宫保鸡丁";
    }
};

// 鱼香肉丝的命令
class YXRSCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeYXRS();
    }
    string name() override
    {
        return "鱼香肉丝";
    }
};

// 红烧排骨的命令
class HSPGCommand : public AbstractCommand
{
public:
    using AbstractCommand::AbstractCommand;
    void excute() override
    {
        m_cooker->makeHSPG();
    }
    string name() override
    {
        return "红烧排骨";
    }
};

可以看到在这四个子类中,分别重写父类的纯虚函数excute(),在该函数内部通过关联的厨师对象分别制作出了地三鲜、宫保鸡丁、鱼香肉丝、红烧排骨

顾客下单就是命令模式中的命令,这些命令的接收者是厨师,命令被分离出来实现了和厨师类的解耦合。通过这种方式可以控制命令执行的时机,毕竟厨师都是在顾客点餐完毕之后才开始炒菜的。

2.3 服务员路飞

顾客点餐并不直接和厨师产生交集,而是通过服务员完成的,所以通过服务员类需要实现点餐、沟通、取消订单、结账等功能,下面是关于路飞这个服务员类的定义:

C++

// 服务器路飞 - 命令的调用者
class WaiterLuffy
{
public:
    // 下单
    void setOrder(int index, AbstractCommand* cmd)
    {
        cout << index << "号桌点了" << cmd->name() << endl;
        if (cmd->name() == "鱼香肉丝")
        {
            cout << "没有鱼了, 做不了鱼香肉丝, 点个别的菜吧..." << endl;
            return;
        }
        // 没找到该顾客
        if (m_cmdList.find(index) == m_cmdList.end())
        {
            list<AbstractCommand*> mylist{ cmd };
            m_cmdList.insert(make_pair(index, mylist));
        }
        else
        {
            m_cmdList[index].push_back(cmd);
        }
    }
    // 取消订单
    void cancelOrder(int index, AbstractCommand* cmd)
    {
        if (m_cmdList.find(index) != m_cmdList.end())
        {
            m_cmdList[index].remove(cmd);
            cout << index << "号桌, 撤销了" << cmd->name() << endl;
        }
    }
    // 结账
    void checkOut(int index)
    {
        cout << "第[" << index << "]号桌的顾客点的菜是: 【";
        for (const auto& item : m_cmdList[index])
        {
            cout << item->name();
            if (item != m_cmdList[index].back())
            {
                cout << ", ";
            }
        }
        cout << "】" << endl;
    }
    void notify(int index)
    {
        for (const auto& item : m_cmdList[index])
        {
            item->excute();
            cout << index << "号桌" << endl;
        }
    }
private:
    // 存储顾客的下单信息
    map<int, list<AbstractCommand*>> m_cmdList;
};

在路飞对应的服务员类中,通过一个map 容器保存了所有顾客的下单信息,key 值是顾客就餐的餐桌编号, value 值存储的是顾客所有的点餐信息。并且这个 value 是一个 list 容器,用于存储某个顾客的所有的点餐信息。

顾客点餐的时候,每点一个菜都会对应一个AbstractCommand* 类型的命令对象,这个类有很多子类,在容器中实际存储的是这个类的子类对象,此处用到了多态。

在命令模式中,服务员类是命令的调用者,顾客点餐完成之后服务员调用这些命令,命令的接收者也是执行者 -- 厨师就开始给顾客做菜了。

2.4 大快朵颐

万事俱备只欠东风,点餐结束经过短暂的等待,就可以享用美食了:

C++
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;
}

程序输出的结果:

C++
=================== 开始点餐 ===================
1号桌点了鱼香肉丝
没有鱼了, 做不了鱼香肉丝, 点个别的菜吧...
1号桌点了地三鲜
1号桌点了宫保鸡丁
1号桌点了红烧排骨
2号桌点了地三鲜
2号桌点了宫保鸡丁
2号桌点了红烧排骨
=================== 撤销订单 ===================
1号桌, 撤销了地三鲜
=================== 开始烹饪 ===================
开始烹饪宫保鸡丁...1号桌
开始烹饪红烧排骨...1号桌
开始烹饪地三鲜...2号桌
开始烹饪宫保鸡丁...2号桌
开始烹饪红烧排骨...2号桌
=================== 结账 ===================
第[1]号桌的顾客点的菜是: 【宫保鸡丁, 红烧排骨】
第[2]号桌的顾客点的菜是: 【地三鲜, 宫保鸡丁, 红烧排骨】

有了这款点餐软件,路飞表示再也没有因为点餐出错而被扣工资了。

3. 结构图

最后根据上面的例子把对应的UML类图画一下(学会了命令模式之后,应该先画UML类图,再写程序。)

命令模式最大的特点就是松耦合设计,它有以下几个优势:

  1. 使用这种模式可以很容易地设计出一个命令队列(对应路飞类中的点餐列表)
  2. 可以很容易的将命令记录到日志中(对应例子中的账单信息)
  3. 允许接收请求的一方决定是否要否决请求(对应例子中的鱼香肉丝)
  4. 可以很容易的实现对请求的撤销和重做(对应例子中的撤单函数)

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/command/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


3.迭代器模式【百兽海贼团】

1. 凯多的烦恼

百兽海贼团是由原新世界的四皇之一"百兽"凯多创建并领导的海贼团。其成员多为恶魔果实动物系能力者(或人造恶魔果实能力者),被称之为最强大的海贼团体,其大本营驻扎在新世界的和之国海域的鬼之岛上。其成员按照职位高低来分类,分为:总督(1人),大看板(3人),真打,编号者,给赋者,爆笑者,等待者,小弟更是不计其数。

通过上面的介绍可以得知,百兽海贼团的结构是一个树状结构,而且人员众多。假设我们要将百兽海贼团中的成员逐一遍历一遍,应该如何处理呢?

如果按照海贼团的等级划分来存储这些团员信息,遍历他们有两种方式:深度优先搜索广度优先搜索;如果存储海贼团成员信息的时候使用的是线性表或者其他结构,现有的遍历算法可能就不再适用了,最优的解决方案就是将集合与它对应的遍历算法解耦

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

关于迭代器模式,现实生活中对应的场景也有很多,比如:

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

2. 同志们辛苦了

凯多虽然打架很厉害,但是如果问他手下有多少人,估计他也不知道,虽然尾田说凯多的百兽海贼团共计约3万人,但凯多自己肯定也没有数过,下面我们写个程序替凯多检阅一下它的队伍。

2.1 集结部队

迭代器模式主要是用来进行数据的遍历,对于凯多的百兽海贼团来说,需要有一个容器将海贼团的成员存储起来,这里我们选择使用链表(因为STL容器都携带了各自的迭代器,不需要我们再为其提供新的迭代器)。

头文件 MyList.h

C++
#pragma once
#include <string>
using namespace std;
// 定义一个链表节点
struct Node
{
    Node(string n) : name(n) {}
    string name = string();
    Node* next = nullptr;
    Node* prev = nullptr;
};

// 双向链表
class MyList
{
public:
    inline int getCount()
    {
        return m_count;
    }

    inline Node* head()
    {
        return m_head;
    }

    inline Node* tail()
    {
        return m_tail;
    }

    Node* insert(Node* item, string data);
    Node* pushFront(string data);
    Node* pushBack(string data);

private:
    Node* m_head = nullptr;
    Node* m_tail = nullptr;
    int m_count = 0;
};

源文件 MyList.cpp

C++

#include "MyList.h"

Node* MyList::insert(Node* item, string data)
{
    Node* node = nullptr;
    if (item == m_head)
    {
        node = pushFront(data);
    }
    else
    {
        node = new Node(data);
        node->next = item;
        node->prev = item->prev;
        // 重新连接
        item->prev->next = node;
        item->prev = node;
        m_count++;
    }

    m_count++;
    return node;
}

Node* MyList::pushFront(string data)
{
    Node* node = new Node(data);
    // 空链表
    if (m_head == nullptr)
    {
        m_head = m_tail = node;
    }
    else
    {
        node->next = m_head;
        m_head->prev = node;
        m_head = node;
    }
    m_count++;
    return node;
}

Node* MyList::pushBack(string data)
{
    Node* node = new Node(data);
    // 空链表
    if (m_tail == nullptr)
    {
        m_head = m_tail = node;
    }
    else
    {
        m_tail->next = node;
        node->prev = m_tail;
        m_tail = node;
    }
    m_count++;
    return node;
}

上面代码实现的是一个双向链表,这样就可以把百兽海贼团所有成员集结到一起了。

2.2 准备检阅

如果想要遍历上面的链表集合,有两种方式:一种是正向遍历,一种是逆向遍历,不论哪一种遍历方式它们都对应相同的操作接口,所以需要先提供一个抽象的迭代器基类。通过代器接口访问上面的双向链表的时候,我们只知道它是一个容器,至于其内部的数据结构已经全部被隐藏了。

抽象的迭代器类(Iterator.h)

C++
// 抽象的迭代器类
class Iterator
{
public:
    Iterator(MyList* mylist) : m_list(mylist) {}
    Node* current()
    {
        return m_current;
    }
    virtual Node* first() = 0;
    virtual Node* next() = 0;
    virtual bool isDone() = 0;
    virtual ~Iterator() {}
protected:
    MyList* m_list = nullptr;
    Node* m_current = nullptr;
};

在这个迭代器基类的内部包含一个双向链表的实例对象 m_list,通过迭代器类遍历双向链表的时候:

  • 通过isDone()函数判断遍历是否结束了
  • 通过current()函数得到遍历到的当前节点
  • 在进行正向遍历的时候:
    • 通过first()函数得到链表的头结点
    • 通过next()函数得到当前节点的后继节点
  • 在进行逆向遍历的时候:
    • 通过first()函数得到链表的尾结点
    • 通过next()函数得到当前节点的前驱节点

正向遍历和逆向遍历

C++
// 正向迭代器
class ForwardIterator : public Iterator
{
public:
    using Iterator::Iterator;
    Node* first() override
    {
        m_current = m_list->head();
        return m_current;
    }
    Node* next() override
    {
        m_current = m_current->next;
        return m_current;
    }
    bool isDone() override
    {
        return m_current == m_list->tail()->next;
    }
};

// 逆向迭代器
class ReverseIterator : public Iterator
{
public:
    using Iterator::Iterator;
    Node* first() override
    {
        m_current = m_list->tail();
        return m_current;
    }
    Node* next() override
    {
        m_current = m_current->prev;
        return m_current;
    }
    bool isDone() override
    {
        return m_current == m_list->head()->prev;
    }
};

在子类ForwardIterator ReverseIterator 中分别重写父类的纯虚函数,实现了对双向链表的正向遍历和逆向遍历。通过编写的代码我们可以非常清晰的看到,其实所谓的迭代器模式就是专门针对某个容器的遍历提供对应的操作类,通过迭代器类的封装使对应的容器的遍历操作变得简单,并且隐藏了容器的内部细节。

2.3 小插曲

现在容器类(也就是上面的双向链表类)和遍历容器的迭代器类都定义出来了,而且迭代器类是为容器量身定制的,如果想遍历容器,那么就需要让这个容器能够提供出一个可用的迭代器对象,所以还需要在链表类中添加一个获取或者创建迭代器对象的函数:

头文件 MyList.h

C++
#pragma once
#include <string>
using namespace std;
// 定义一个链表节点
struct Node
{
    Node(string n) : name(n) {}
    string name = string();
    Node* next = nullptr;
    Node* prev = nullptr;
};

class Iterator;
// 双向链表
class MyList
{
public:
    inline int getCount()
    {
        return m_count;
    }

    inline Node* head()
    {
        return m_head;
    }

    inline Node* tail()
    {
        return m_tail;
    }

    Node* insert(Node* item, string data);
    Node* pushFront(string data);
    Node* pushBack(string data);
    Iterator* getIterator(bool isReverse = false);

private:
    Node* m_head = nullptr;
    Node* m_tail = nullptr;
    int m_count = 0;
};

由于迭代器类Iterator和链表类MyList是相互包含的关系,所以尽量不要让这两个类的头文件互相包含,在上面的MyList 类第13行只是对 Iterator 迭代器类进行了声明,保证编译器能够识别出第36行的返回值类型即可。

源文件 MyList.cpp

C++

#include "MyList.h"
#include "Iterator.h"

Node* MyList::insert(Node* item, string data)
{
    Node* node = nullptr;
    if (item == m_head)
    {
        node = pushFront(data);
    }
    else
    {
        node = new Node(data);
        node->next = item;
        node->prev = item->prev;
        // 重新连接
        item->prev->next = node;
        item->prev = node;
        m_count++;
    }
    return node;
}

Node* MyList::pushFront(string data)
{
    Node* node = new Node(data);
    // 空链表
    if (m_head == nullptr)
    {
        m_head = m_tail = node;
    }
    else
    {
        node->next = m_head;
        m_head->prev = node;
        m_head = node;
    }
    m_count++;
    return node;
}

Node* MyList::pushBack(string data)
{
    Node* node = new Node(data);
    // 空链表
    if (m_tail == nullptr)
    {
        m_head = m_tail = node;
    }
    else
    {
        m_tail->next = node;
        node->prev = m_tail;
        m_tail = node;
    }
    m_count++;
    return node;
}

Iterator* MyList::getIterator(bool isReverse)
{
    Iterator* iterator = nullptr;
    if (isReverse)
    {
        iterator = new ReverseIterator(this);
    }
    else
    {
        iterator = new ForwardIterator(this);
    }
    return iterator;
}

在这个MyList 类的源文件中包内含了迭代器类的头文件 Iterator.h,这样在getIterator()函数中就可以根据参数的值创建出一个正向迭代器的实例对象或者一个逆向迭代器的实例对象了,有了这个迭代器对象就可以通过它完成对双向链表的遍历。

2.4 开始检阅

我们先把凯多的百兽海贼团成员初始化到链表中,然后就可以让凯多来检阅队伍了:

C++
int main()
{
    vector<string> nameList{ 
        "烬", "奎因", "杰克", "福兹·弗", "X·德雷克",
        "黑色玛利亚", "笹木", "润媞", "佩吉万",
        "一美", "二牙", "三鬼", "四鬼", "五鬼",
        "六鬼", "七鬼", "八茶", "九忍","十鬼"
    };
    MyList mylist;
    for (int i = 0; i < nameList.size(); ++i)
    {
        mylist.pushBack(nameList.at(i));
    }
    // 遍历
    Iterator* it = mylist.getIterator(true);
    cout << "检阅开始, 凯多: 同志们辛苦啦~~~~~" << endl;
    for (auto begin = it->first(); !it->isDone(); it->next())
    {
        cout << "   " << it->current()->name << "say: 为老大服务!!! " << endl;
    }
    cout << endl;
    delete it;
    return 0;
}

在上面的代码中是进行了逆向遍历,检阅的顺序和nameList 名单中的顺序是相反的,最终输出的结果如下:

C++
检阅开始, 凯多: 同志们辛苦啦~~~~~
   十鬼say: 为老大服务!!!
   九忍say: 为老大服务!!!
   八茶say: 为老大服务!!!
   七鬼say: 为老大服务!!!
   六鬼say: 为老大服务!!!
   五鬼say: 为老大服务!!!
   四鬼say: 为老大服务!!!
   三鬼say: 为老大服务!!!
   二牙say: 为老大服务!!!
   一美say: 为老大服务!!!
   佩吉万say: 为老大服务!!!
   润媞say: 为老大服务!!!
   笹木say: 为老大服务!!!
   黑色玛利亚say: 为老大服务!!!
   X·德雷克say: 为老大服务!!!
   福兹·弗say: 为老大服务!!!
   杰克say: 为老大服务!!!
   奎因say: 为老大服务!!!
   烬say: 为老大服务!!!

3. 结构图

最后根据上面的例子将UML类图画一下(学会迭代器模式之后,应该先画UML类图再写程序。

迭代器模式是一个很经典的模式。所以没必要重复的去造这个轮子,成型的类库都非常好的实现了迭代器模式,在使用这些类库提供的容器时,并不需要我们亲自去实现对应的迭代器,比如 STL。但是,打铁还需自身硬,对于这些必备技能我们是没有理由不去学习和掌握的。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/iterator/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


4.中介者模式【世界政府】

1. 中介者

在海贼世界中,世界政府是拥有170个以上加盟国,依靠牢固的结盟而生成的最大规模的国际组织,以"世界"自居,支配整个世界,守卫掌管司法世界的秩序。世界政府以"绝对正义"的名义维护着世界的安全,为了铲除邪恶以及非法的事物可以不择手段,甚至杀害无辜的人,至于该事物是否"正义"则由政府说了算。

虽然在海贼的世界格局是非常混乱的,但是如果没有世界政府的存在它将更加混乱。以程序猿的视角来看,每个国家都是一个对象,一个国家需要和其它的很多国家都产生交集,这就难免产生冲突、掠夺和战争。如果有世界政府的存在,就可以在一定程度上避免各个国家之间的正面直接接触(对象解耦),还能起到一定的调节作用。

关于世界政府的这种组织形式,在设计模式中被称之为中介者模式。中介者模式可以减少对象之间混乱无序的依赖关系,从而使其耦合松散,限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。

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

中介者对象的设计,使得系统的结构体不会因为新对象的引入造成大量的修改工作。现实生活中关于中介者模式的应用也有很多,以下这些都是中介者:

  • 联合国

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

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

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

2. 愿世界和平

2.1 国家

世界政府有170个以上加盟国,每个国家都有自己的诉求,所以必须得给这些国家提供一个抽象的基类,这个抽象类的定义如下:

C++
// 抽象国家类
class Country
{
public:
    Country(){}
    // 发表声明
    virtual void declare(string msg, string country) = 0;
    virtual void setMessage(string msg) = 0;
    virtual string getName() = 0;
};

在这个基类中有三个纯虚函数,分别是:

  • declare() :各个国家可以通过这个函数发表不同的声明,强调立场,维护自身利益。
  • setMessage() :通过这个函数得到外界发布的关于自己国家的信息
  • getName():得到当前国家的名字

2.2 润滑剂

世界政府充当了一个中间人的角色,用于调解各个国家之间的关系。但是有一些国家并没有屈服于世界政府的淫威,加入这个组织,他们走到了世界政府的对立面加入到了路飞的老爸蒙奇·D·龙领导的革命军组织,试图推翻世界政府的统治。

不论是革命军还是世界政府他们都属于某一个组织,都可以充当中介者的角色,所以关于中介者类也可以定义出一个抽象的基类:

C++
// 抽象的中介机构
class MediatorOrg
{
public:
    void addMember(Country* country);
    virtual void declare(string msg, Country* country, string name = string()) = 0;
    virtual ~MediatorOrg() {}
protected:
    map<string, Country*> m_countryMap;
};

这个中介者的抽象类中包含了一个受保护的map 容器对象,key 值存储的是国家的名字,value 值存储的是国家的对象。这样,和中介者对象关联的所有国家对象就可以全部被存储起来了。

  • addMember() :添加国家对象到中介者对象中并保存起来。
  • declare() :中介者对象可以将某个国家的诉求转达给其他国家,这是中介者类中的数据中转函数。

世界政府

世界政府就是一个中介者实例,所以可以让世界政府类继承上面的中介者抽象类,关于这个类的定义如下:

头文件 Mediator.h

C++
// 世界政府
class Country;
class WorldGovt : public MediatorOrg
{
public:
    void declare(string msg, Country* country, string name = string()) override;
};

在头文件中只需要对Country 国家类 进行声明即可, 不要包含它的头文件,否则会造成两个类的头文件交叉互相包含。

源文件 Mediator.cpp

C++
#include <iostream>
#include "Mediator.h"
#include "Country.h"
using namespace std;

// 基类的成员添加函数
void MediatorOrg::addMember(Country* country)
{
    m_countryMap.insert(make_pair(country->getName(), country));
}

// 在子类中重写发表声明的函数 
void WorldGovt::declare(string msg, Country* country, string name)
{
    if (m_countryMap.find(name) != m_countryMap.end())
    {
        string str = msg + "【来自: " + country->getName() + "】";
        m_countryMap[name]->setMessage(str);
    }
}

在世界政府类中,通过declare() 会将一个国家的声明转发给另一个国家

  • msg:发布声明的国家发布的信息
  • country:发布声明的国家的对象
  • name:中介者需要将声明转达给这个国家

在这个类的源文件中就可以包含国家类的头文件Country.h了,如果不包含头文件就无法识别出国家类提供的成员函数。

革命军

头文件 Mediator.h

C++
// 革命军
class GeMingArmy : public MediatorOrg
{
public:
    void declare(string msg, Country* country, string name = string()) override;
};

源文件 Mediator.cpp

C++
// 在子类中重写发表声明的函数 
void GeMingArmy::declare(string msg, Country* country, string name)
{
    string str = msg + "【来自: " + country->getName() + "】";
    for (const auto& item : m_countryMap)
    {
        if (item.second == country)
        {
            continue;
        }
        item.second->setMessage(str);
    }
}

在革命军类中,通过declare() 会将一个国家的声明转发给其他的所有国家。

2.3 入伙

有了世界政府和革命军另个中介者组织之后,世界各国就可以站队了,所以哪个国家要加入哪个组织需要明确的指定出来,我们来修改一下国家类的基类:

C++
// 抽象国家类
class Country
{
public:
    Country(MediatorOrg* mediator) : m_mediator(mediator) {}
    // 发表声明
    virtual void declare(string msg, string country) = 0;
    virtual void setMessage(string msg) = 0;
    virtual string getName() = 0;
    virtual ~Country() {}
protected:
    MediatorOrg* m_mediator = nullptr;
};

在创建国家对象的时候,需要指定出其认可的中介者对象,这样就能知道这个国家的立场了。

接下来就是基于这个抽象的国家类定义出一些可被实例化的子国家类:

C++

#pragma once
#include <string>
#include <iostream>
#include "Mediator.h"
using namespace std;

// 抽象国家类
class Country
{
public:
    Country(MediatorOrg* mediator) : m_mediator(mediator) {}
    // 发表声明
    virtual void declare(string msg, string country) = 0;
    virtual void setMessage(string msg) = 0;
    virtual string getName() = 0;
    virtual ~Country() {}
protected:
    MediatorOrg* m_mediator = nullptr;
};

// 阿拉巴斯坦
class Alabasta : public Country
{
public:
    using Country::Country;
    void declare(string msg, string country) override
    {
        m_mediator->declare(msg, this, country);
    }
    void setMessage(string msg) override
    {
        cout << "阿拉巴斯坦得到的消息: " << msg << endl;
    }
    string getName() override
    {
        return "阿拉巴斯坦";
    }
};

// 德雷斯罗萨
class Dressrosa : public Country
{
public:
    using Country::Country;
    void declare(string msg, string country) override
    {
        m_mediator->declare(msg, this, country);
    }
    void setMessage(string msg) override
    {
        cout << "德雷斯罗萨得到的消息: " << msg << endl;
    }
    string getName() override
    {
        return "德雷斯罗萨";
    }
};

// 露露西亚王国
class Lulusia : public Country
{
public:
    using Country::Country;
    void declare(string msg, string country) override
    {
        m_mediator->declare(msg, this, country);
    }
    void setMessage(string msg) override
    {
        cout << "露露西亚得到的消息: " << msg << endl;
    }
    string getName() override
    {
        return "露露西亚";
    }
};

// 卡玛巴卡王国
class Kamabaka : public Country
{
public:
    using Country::Country;
    void declare(string msg, string country) override
    {
        m_mediator->declare(msg, this, country);
    }
    void setMessage(string msg) override
    {
        cout << "卡玛巴卡得到的消息: " << msg << endl;
    }
    string getName() override
    {
        return "卡玛巴卡";
    }
};

上面一共定义了四个字国家类分别是Alabasta、Dressrosa、Lulusia、Kamabaka,这四个国家在发布声明的时候不是直接发布,而是将消息给到了他们所信任的中介者对象,通过这个中介者对象将消息发布给对应的其他国家,这样国家和国家直接就实现了解耦合,更利于程序的维护和扩展。

2.4 交流

最后就可以让上面的四个国家加入组织,通过中介者对象进行交流了:

C++
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;
}

上面的四个国家有两个国家加入了世界政府,有两个国家加入了革命军,最后看一下他们交流的结果:

C++
德雷斯罗萨得到的消息: 德雷斯罗萨倒卖军火, 搞得我国连年打仗, 必须给个说法!!!【来自: 阿拉巴斯坦】
阿拉巴斯坦得到的消息: 天龙人都和我多弗朗明哥做生意, 你算老几, 呸!!!【来自: 德雷斯罗萨】
======================================
卡玛巴卡得到的消息: 我草, 我的国家被伊姆毁灭了!!!【来自: 露露西亚】

3. 结构图

最后根据上面的例子把中介者模式对应的UML类图画一下(学会了中介者模式之后,应该先画UML类图再写程序。)

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/mediator/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


5.备忘录模式【历史正文】

1. 抹除奥哈拉

在海贼世界中,大约800年前,存在着一个繁荣一时的"巨大王国",该王国在败给了世界政府联军后便不复存在,因此也留下了一段"空白一百年"的历史。三叶草博士认为,这个国家在败给世界政府的联军之前,就已经做好的失败的思想准备,为了把思想留给未来,于是把所有真相都刻在了石头上,也是就历史正文

22年前,世界政府发现奥哈拉的考古学者们在对历史正文进行研究,于是派人前去逮捕他们。随后,CP9长官斯潘达因对奥哈拉发动了屠魔令,整座岛屿仅妮可·罗宾一人逃走 。翌年,"奥哈拉"这个名字已从地图上消失。

其实历史正文就是对空白的一百年历史的记录,我们可以认为这就是一份备忘录,解读出了历史正文就等于还原了历史,里边肯定记录着世界政府干过的一些见不得光的事情,不得不说"巨大王国"的人们还是很聪明的。

在设计模式中也有一种备份数据的模式叫做备忘录模式,关于这种模式的定义是这样的在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后在需要的时候就可以将该对象恢复到原先保存的状态。

关于备忘录模式在现实生活场景中也比较常用,比如:

  • 游戏的进度存档

  • 文本编辑器

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

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

2. 勿忘历史

2.1 佛狸祠

海贼中的世界政府消灭了巨大王国,又想要毁灭其历史(要杀死所有能读懂历史正文的历史学家),让全世界的人们都认为世界政府就是救世主,就是绝对的正义。在我们真实世界中,这种事情比比皆是,在辛弃疾的《永遇乐·京口北固亭怀古》中写道可堪回首,佛狸祠下,一片神鸦社鼓。(佛狸是北魏拓跋焘的小名,曾占据南宋北方大半江山,佛狸祠就是拓跋焘的行宫)可是后来的老百姓们却把佛狸当作一位神祇来奉祀,并没有去审查这个神的来历,因为他们都忘了历史。

那时候的老百姓可能买不起书,上不起学,但是现在的我们就不一样了,我们和宋朝人民一样都有过一段屈辱的历史,所以作为程序猿的我要通过备忘录模式写一段程序,记录下这段历史,小鬼子亡我之心依旧不死,我辈要提防,不能让现在的靖国神厕变成下一个佛狸祠。

2.2 解构历史

历史的进程有一定的脉络,作为程序猿的我们可以把历史拆分为这么几个部分:

  1. 历史中的亲历者(个人或群体)
  2. 历史中发生的事情的来龙去脉
  3. 记录历史的人

这三部分数据也是备忘录模式中的三要素:事件的主体、事件的内容、事件的记录者。在事件主体上发生的事情就是事件的内容,事件的内容通过事件记录者进行备份。很显然,在备忘录模式中将事件的主体和事件的内容进行解耦合更有利于程序的扩展和维护。根据上面的描述,我们就可以把这三者之间的对应关系通过UML类图描述出来了:

血泪史

我国的近代史就是一部血泪史,小日本居然都敢下克上,妄图吞并我中华。对于历史上发生的每一个事件,我们需要将其清楚地记录下来,所以在备忘录模式中需要提供一个历史类,这个历史类并不需要记录所有的历史事件,而是用于记录某一个历史事件,原因很简单,这些事件是一个个发生的,所以就可以发生一件记录一件。关于历史类的定义如下:

C++
// 历史备忘录类
class History
{
public:
    History(string msg) : m_msg(msg) {}
    string getHistory()
    {
        return m_msg;
    }
private:
    string m_msg;
};

这个类很简单,在创建历史对象的时候,通过构造函数将具体的历史事件封装起来,保存到当前对象中,通过getHistory()可以得到关于具体的历史事件的描述。

脚盆鸡

在备忘录模式中,将小日本和它们做的事情分开了,小日本的虚伪之处就在于只要犯了错就鞠躬道歉,但就是不改并且不承担责任,就这德行居然对曾经杀死的中国人连个鞠躬都没有。所以在这个鬼子类中,需要将它们做的事情封装成一个历史类对象,并且还需要提供一个还原历史的函数,让它们无法否认历史,这个类的定义如下:

C++
// 鬼子
class JiaoPenJi
{
public:
    void setState(string msg)
    {
        m_msg = msg;
    }

    string getState()
    {
        return m_msg;
    }

    void beiDaddyDa()
    {
        cout << "脚盆鸡被兔子狠狠地揍了又揍, 终于承认了它们之前不承认的这些罪行: " << endl;
    }

    History* saveHistory()
    {
        return new History(m_msg);
    }
    void getStateFromHistory(History* history)
    {
        m_msg = history->getHistory();
    }
private:
    string m_msg;
};

再介绍一下这个类中提供的几个成员函数:

  • setState(string msg):代表鬼子在我国犯下了某一个罪行,参数对应的就是相关的描述
  • getState():得到鬼子犯下的罪行的相关信息
  • History* saveHistory():将鬼子的罪行封装成一个历史对象,并通过返回值传出
  • getStateFromHistory(History* history):从一个历史对象中,读出相关的历史信息

我们并没有在这个类中将相关的历史事件记录下来,这种事儿鬼子肯定也是不会干的,所以再次进行解耦,把记录历史的任务交给了正义的了解历史的记录者。

记录者

对于发生的历史事件,需要记录者将其一条条保存下来,使其得以保存和被后人所知。关于这个记录者我们可以将其理解为一部被拟人化的史书,我们可以往里边添加历史信息也可以从中获取历史信息,所以这个类的定义如下:

C++
// 记录者
class Recorder
{
public:
    void addHistory(int index, History* history)
    {
        m_history.insert(make_pair(index, history));
    }
    History* getHistory(int index)
    {
        if (m_history.find(index) != m_history.end())
        {
            return m_history[index];
        }
        return nullptr;
    }
private:
    map<int, History*> m_history;
};

在这个类中,将所有的历史信息通过map 容器存储了起来

  • 通过addHistory 函数 添加历史信息,并保存
  • 通过getHistory 函数 从备份信息中得到想要的历史信息

2.3 罪证

纵观中国历史,古人告诉我们对付倭国的这帮畜生只有一个办法就是使劲揍,打的越疼,它们越顺从。最后来看一下,倭国被收拾之后承认的对我国犯下的累累罪行:

C++
int main()
{
    vector<string> msg{
        "不承认偷了中国的中医!!!",
        "不承认发动了侵华战争!!!",
        "不承认南京大屠杀!!!!",
        "不承认琉球群岛和钓鱼岛是中国的领土!!!",
        "不承认731部队的细菌和人体试验!!!",
        "不承认对我国妇女做出畜生行为!!!",
        "不承认从中国掠夺的数以亿计的资源和数以万计的文物!!!",
        "我干的龌龊事儿罄竹难书, 就是不承认......"
    };
    JiaoPenJi* jiaopen = new JiaoPenJi;
    Recorder* recorder = new Recorder;
    // 把小日本的罪行记录下来
    for (int i = 0; i < msg.size(); ++i)
    {
        jiaopen->setState(msg.at(i));
        recorder->addHistory(i, jiaopen->saveHistory());
    }
    jiaopen->beiDaddyDa();
    for (int i = 0; i < msg.size(); ++i)
    {
        jiaopen->getStateFromHistory(recorder->getHistory(i));
        cout << "  -- " << jiaopen->getState() << endl;
    }
    return 0;
}

输出的结果如下:

C++
脚盆鸡被兔子狠狠地揍了又揍, 终于承认了它们之前不承认的这些罪行:
  -- 不承认偷了中国的中医!!!
  -- 不承认发动了侵华战争!!!
  -- 不承认南京大屠杀!!!!
  -- 不承认琉球群岛和钓鱼岛是中国的领土!!!
  -- 不承认731部队的细菌和人体试验!!!
  -- 不承认对我国妇女做出畜生行为!!!
  -- 不承认从中国掠夺的数以亿计的资源和数以万计的文物!!!
  -- 我干的龌龊事儿罄竹难书, 就是不承认......

吾辈当自强,牢记历史,振兴中华,锤死小人本!!!当然这要从写好程序开始,最后总结一下备忘录模式的应用场景:

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/memento/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


6.观察者模式【摩根斯】

1. 新闻大亨

摩根斯是海贼世界中一个比较神秘的人物,他是世界经济新闻社的社长,人称"大新闻摩根斯"。他总是能非常轻松地搞到第一手情报,将其印成报纸,并由自家的送报鸟把报纸送到世界各地。

先不去深究摩根斯是如何快速得到世界各地的动态以及得到一些秘密情报,暂且认为他是豢养了一支空中狗仔队,在全世界的上空进行24小时无死角监视。对于摩根斯的新闻社我们可以将其看作是消息的发布者,对于购买报纸的各国人民或者是海上的海贼,我们可以将他们看作是消息的观察者或者订阅者。

在设计模式中也有一种类似的描述行为的模式,叫做观察者模式。观察者模式允许我们定义一种订阅机制,可在对象事件发生时通知所有的观察者对象,使它们能够自动更新。观察者模式还有另外一个名字叫做"发布-订阅"模式。

观察者模式在日常生活中也很常见,比如:

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

2. 辛苦了,送报鸟

2.1 发布者

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

  1. 添加订阅者,将所有的订阅者存储起来
  2. 删除订阅者,将其从订阅者列表中删除
  3. 将消息发送给订阅者(发通知)

根据上述信息,我们就可以先创建出一个发布者的抽象类:

头文件 NewsAgency.h

C++
// 声明订阅者类, 只是做了声明, 并没有包含这个类的头文件
class Observer;
// 新闻社
class NewsAgency
{
public:
    void attach(Observer* ob);
    void deatch(Observer* ob);
    virtual void notify(string msg) = 0;
    virtual ~NewsAgency() {};
protected:
    // 订阅者列表
    list<Observer*> m_list;
};

对于上面定义的类做以下的细节说明:

  • 第2行:class Observer,此时这个Observer 订阅者类还不存在,此处只是做一个声明,让编译器能够通过编译
  • 第13行:将所有的订阅者对象存储到STL的list 容器中
  • 第7行:添加一个订阅者到list 容器中
  • 第8行:从list 容器中删除一个订阅者
  • 第9行:将通知信息发送给list 容器中的所有订阅者

源文件 NewsAgency.cpp

C++
#include "NewsAgency.h"
#include "Observer.h"	// 在源文件中包含该头文件
#include <iostream>
void NewsAgency::attach(Observer* ob)
{
    m_list.push_back(ob);
}

void NewsAgency::deatch(Observer* ob)
{
    m_list.remove(ob);
}

在头文件中只是对 Observer 类进行了声明定义了这种类型的指针变量,在源文件中需要调用 Observer 类提供的 API,所以必须要包含这个类的头文件。这么处理的目的是为了避免两个相互关联的类他们的头文件相互包含。

摩根斯新闻

摩根斯的新闻社可以作为上面NewsAgency 类的子类,在子类中重写父类的纯虚函数notify()方法就可以了。

头文件 NewsAgency.h

C++
// 摩根斯的新闻社
class Morgans : public NewsAgency
{
public:
    void notify(string msg) override;
};

源文件 NewsAgency.cpp

C++
void Morgans::notify(string msg)
{
    cout << "摩根斯新闻社报纸的订阅者一共有<" << m_list.size() << ">人" << endl;
    for (const auto& item : m_list)
    {
        item->update(msg);	// 订阅者类的定义在下面
    }
}

八卦新闻

八卦新闻社的处理思路和摩根斯新闻社的处理思路是完全一样的,代码如下:

头文件 NewsAgency.h

C++
// 八卦新闻
class Gossip : public NewsAgency
{
public:
    void notify(string msg) override;
};

源文件 NewsAgency.cpp

C++
void Gossip::notify(string msg)
{
    cout << "八卦新闻社报纸的订阅者一共有<" << m_list.size() << ">人" << endl;
    for (const auto& item : m_list)
    {
        item->update(msg);
    }
}

2.2 订阅者

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

C++
#pragma once
#include <string>
#include <iostream>
#include "NewsAgency.h"
using namespace std;

// 抽象的订阅者类
class Observer
{
public:
    Observer(string name, NewsAgency* news) :m_name(name), m_news(news) 
    {
        m_news->attach(this);
    }
    void unsubscribe()
    {
        m_news->deatch(this);
    }
    virtual void update(string msg) = 0;
    virtual ~Observer() {}
protected:
    string m_name;
    NewsAgency* m_news;
};

下面介绍一下这个抽象的观察者类中的一些细节:

  • 第11行:需要通过构造函数给观察者类提供一个信息的发布者
  • 第13行:通过发布者对象将观察者对象存储了起来,这样就可以收到发布者推送的消息了。
  • 第15行:观察者取消订阅,取消之后将不再接收订阅消息。
  • 第19行:观察者得到最新消息之后,用于更新自己当前的状态。

有了上面的抽象类,我们就可以再添加几个订阅者的子类:

蒙奇·D·龙

C++
class Dragon : public Observer
{
public:
    using Observer::Observer;
    void update(string msg) override
    {
        cout << "@@@路飞的老爸革命军龙收到消息: " << msg << endl;
    }
};

香克斯

C++
class Shanks : public Observer
{
public:
    using Observer::Observer;
    void update(string msg) override
    {
        cout << "@@@路飞的引路人红发香克斯收到消息: " << msg << endl;
    }
};

巴托洛米奥

C++
class Bartolomeo : public Observer
{
public:
    using Observer::Observer;
    void update(string msg) override
    {
        cout << "@@@路飞的头号粉丝巴托洛米奥收到消息: " << msg << endl;
    }
};

可以看到上面的三个子类非常类似,只是在各自的类中分别重写了update()这个状态更新函数(因为以上是测试程序,所有逻辑比较简单)。

2.3 起飞,送报鸟

最后,我们来演示一下消息从发布到到达订阅者手中的过程,在此要感谢送报鸟的辛勤付出:

C++
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;
}

送报鸟送到订阅者手中的信息是这样的:

C++
摩根斯新闻社报纸的订阅者一共有<2>人
@@@路飞的老爸革命军龙收到消息: 蒙奇·D·路飞成为新世界的新的四皇之一, 赏金30亿贝里!!!
@@@路飞的引路人红发香克斯收到消息: 蒙奇·D·路飞成为新世界的新的四皇之一, 赏金30亿贝里!!!
======================================
八卦新闻社报纸的订阅者一共有<1>人
@@@路飞的头号粉丝巴托洛米奥收到消息: 女帝汉库克想要嫁给路飞, 给路飞生猴子, 哈哈哈...

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

3. 结构图

最后画一下观察者模式对应的UML类图(学会观察者模式之后,要先画UML类图再写程序。)

当然订阅者和发布者可能是没有子类的,因此也就不需要继承了,这个根据实际情况,具体问题具体分析就可以了。

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/observer/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


7.策略模式[蒙奇D路飞]

1. 橡胶人

路飞是要成为海贼王的男人!在小时候因为误食了红发香克斯找到的橡胶恶魔果实成了橡胶人。对于果实能力,路飞现在已经开发出了五个档位。对于路飞而言在战斗的时候,必须要根据敌人的情况来实时制定合适的策略,使用不同的档位的不同招式去应对来自对方的攻击。

路飞在战斗的时候需要制定策略,在设计模式中也有一种和策略相关的模式叫做策略模式。策略模式需要我们定义一系列的算法,并且将每种算法都放入到独立的类中,在实际操作的时候使这些算法对象可以相互替换。

在日常生活中很多时候都需要制定策略,在程序中就可用使用策略模式来处理这些场景,比如:

  • 出行策略,可以选择不同的交通工具:自行车、公交、地铁、自驾
  • 战国时期秦国的外交政策:远交近攻
  • 收复台湾的策略:武统、文统、恩威并施 。。。
  • 电商平台的打折策略:买二赠一、满300减50、购买VIP享8折优惠。。。

2. 百变路飞

作为橡胶人路飞,平时白痴但战斗时头脑异常清醒,会根据敌我双方的形式做出正确的判断:

  • 对手战力弱,使用1档并根据实际情况使用相应的招式
  • 对手战力一般,切换2档并根据实际情况使用相应的招式
  • 对手战力强,切换3档并根据实际情况使用相应的招式
  • 对手战力无敌,切换4档并根据实际情况使用相应的招式
  • 对手战斗逆天,切换5档并根据实际情况使用相应的招式

假设将所有的策略都写到一个类中就会使得路飞这个类过于复杂,而且不容易维护,如果基于策略模式来处理路飞这个类,可以把关于在什么场景下使用什么策略的判断去除,把处理逻辑分散到多个不同的策略类中,这样就可以将复杂的逻辑简化了。

2.1 路飞的形态

根据上面的描述路飞一共有五种形态,不论使用哪种形态都需要制定战斗策略,这一点是不变的,所以我们就可以定义出一个抽象的策略类:

C++
// 抽象的策略类
class AbstractStrategy
{
public:
    virtual void fight(bool isfar = false) = 0;
    virtual ~AbstractStrategy() {}
};

这个抽象类中的fight()函数有一个布尔类型的参数,表示在当前这个状态下是要进行近距离攻击还是远程攻击,参数不同,在这种状态下使用的招式也不同。有了这个抽象的基类,就可以定义出一系列的子类了:

C++

// 一档
class YiDang : public AbstractStrategy
{
public:
    void fight(bool isfar = false) override
    {
        cout << "*** 现在使用的是一档: ";
        if (isfar)
        {
            cout << "橡胶机关枪" << endl;
        }
        else
        {
            cout << "橡胶·攻城炮" <<endl;
        }
    }
};

// 二挡
class ErDang : public AbstractStrategy
{
public:
    void fight(bool isfar = false) override
    {
        cout << "*** 切换成二挡: ";
        if (isfar)
        {
            cout << "橡胶Jet火箭" << endl;
        }
        else
        {
            cout << "橡胶Jet·铳乱打" << endl;
        }
    }
};

// 三挡
class SanDang : public AbstractStrategy
{
public:
    void fight(bool isfar = false) override
    {
        cout << "*** 切换成三挡: ";
        if (isfar)
        {
            cout << "橡胶巨人回旋弹" << endl;
        }
        else
        {
            cout << "橡胶巨人战斧" << endl;
        }
    }
};

// 四挡
class SiDang : public AbstractStrategy
{
public:
    void fight(bool isfar = false) override
    {
        cout << "*** 切换成四挡: ";
        if (isfar)
        {
            cout << "橡胶狮子火箭炮" << endl;
        }
        else
        {
            cout << "橡胶犀牛榴弹炮" << endl;
        }
    }
};

// 五档
class WuDang : public AbstractStrategy
{
public:
    void fight(bool isfar = false) override
    {
        cout << "*** 切换成五挡: 变成尼卡形态可以把物体变成橡胶, 并任意改变物体的形态对其进行攻击!!!" << endl;
    }
};

这五个子类分别对应的路飞的一档、二挡、三挡、四挡、五档,也就是五种不同的策略,在它们内部都重写了从基类继承的纯虚函数fight(),并根据函数的参数做了不同的处理。通过这种方式的拆分就把复杂的问题简单化了,有种兄弟分家的感觉,本来是在同一个屋檐下,分家之后都有了自己的房子,各过各的互不干涉。

2.2 路飞

上面说到了分家,那个这几个儿子的爹是谁呢?没错,就是路飞,这五个子类都是路飞的果实能力,但是现在他们从路飞这个对象中分离出去了。所以现在路飞和这几个技能的状态类之间就变成了组合关系。关于路飞这个类我们可以这样定义:

C++
// 难度级别
enum class Level:char {Easy, Normal, Hard, Experts, Professional};

// 路飞
class Luffy
{
public:
    void fight(Level level, bool isfar = false)
    {
        if (m_strategy)
        {
            delete m_strategy;
            m_strategy = nullptr;
        }
        switch (level)
        {
        case Level::Easy:
            m_strategy = new YiDang;
            break;
        case Level::Normal:
            m_strategy = new ErDang;
            break;
        case Level::Hard:
            m_strategy = new SanDang;
            break;
        case Level::Experts:
            m_strategy = new SiDang;
            break;
        case Level::Professional:
            m_strategy = new WuDang;
            break;
        default:
            break;
        }
        m_strategy->fight(isfar);
    }
    ~Luffy()
    {
        delete m_strategy;
    }
private:
    AbstractStrategy* m_strategy = nullptr;
};

Luffy 类中的fight()方法里边根据参数传递进来的难度级别,路飞在战斗的时候就可以选择开启对应的档位使用相关的招式来击败对手。

2.3 对症下药

路飞在战斗的时候头脑还是非常灵活的,下面来看一下路飞经历过的一些战斗:

C++
int main()
{
    Luffy* luffy = new Luffy;
    cout << "--- 在香波地群岛遇到了海军士兵: " << endl;
    luffy->fight(Level::Easy);
    cout << "--- 在魔谷镇遇到了贝拉米: " << endl;
    luffy->fight(Level::Normal);
    cout << "--- 在司法岛遇到了罗布·路奇: " << endl;
    luffy->fight(Level::Hard);
    cout << "--- 在德雷斯罗萨遇到了多弗朗明哥: " << endl;
    luffy->fight(Level::Experts);
    cout << "--- 在鬼岛遇到了凯多: " << endl;
    luffy->fight(Level::Professional);

    delete luffy;
    return 0;
}

当时战斗的场景是这样的:

C++
--- 在香波地群岛遇到了海军士兵:
*** 现在使用的是一档: 橡胶·攻城炮
--- 在魔谷镇遇到了贝拉米:
*** 切换成二挡: 橡胶Jet·铳乱打
--- 在司法岛遇到了罗布·路奇:
*** 切换成三挡: 橡胶巨人战斧
--- 在德雷斯罗萨遇到了多弗朗明哥:
*** 切换成四挡: 橡胶犀牛榴弹炮
--- 在鬼岛遇到了凯多:
*** 切换成五挡: 变成尼卡形态可以把物体变成橡胶, 并任意改变物体的形态对其进行攻击!!!

3. 结构图

最后画一下策略模式对应的UML类图(学会策略模式之后,要先画UML类图再写程序。)

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/strategy/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


8.状态模式【文斯莫克山治】

1. 厨师山治

山治是文斯莫克家族的第三子,基因改造人,由于小时候未能觉醒能力而被父亲文斯莫克·伽治放逐到东海。遇见恩师哲普后在海上餐厅巴拉蒂担任厨师。为了寻找传说之海ALL BLUE而随草帽一伙踏入伟大航路。

在海上航行的每一天里,最忙的应该就是山治了。他需要准备食材、做早饭、做午饭、做下午茶、做晚饭,一整天都在不同的状态之间忙碌。

在设计模式中有一种和山工作治状态类似的模式叫做状态模式。状态模式就是在一个类的内部会有多种状态的变化,因为状态变化从而导致其行为的改变,在类的外部看上去这个类就像是自身发生了改变一样。

在日常生活中由于内部属性的变化导致外在样貌或者行为发生改变的例子比比皆是,比如:

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

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

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

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

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

状态模式和策略模式比较类似,策略模式中的各个策略是独立的不关联的,但是状态模式下的对象的各种状态可以是独立的也可以是相互依赖的,比如上面关于文章的发布的例子:

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

2. 山治的一天

假设这一天草帽一伙在一直在海上航行,没有遇到海军也没有遇到极端天气,对于船上的成员来说这又是可以大饱口福的一天。

2.1 开始工作

山治作为厨师一整天都在忙碌,我们在这里简单的把他的工作状态划分一下:上午的状态、中午的状态、下午的状态、晚上的状态。不论哪种状态他都是在认真的在完成自己的本职工作,只不过在不同的时间点工作的内容是不一样的。所以,我们可以给这些状态定义一个基类:

C++
// State.h
// 抽象状态
class Sanji;
class AbstractState
{
public:
    virtual void working(Sanji* sanji) = 0;
    virtual ~AbstractState() {}
};

由于这个状态是属于山治的,所以在这个抽象的状态类中通过提供的工作函数working()的参数指定了这个状态的所有者,在这里只是对山治类 Sanji做了一个声明第3行,尽量不要在这个头文件中包含山治的头文件,否则会造成头文件重复包含(因为山治类和状态类需要相互引用对方)。

有了上面的抽象的状态类,就可以基于这个基类把山治全天对应的状态类的子类依次定义出来了:

头文件 State.h

C++
// 上午状态
class ForenoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 中午状态
class NoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 下午状态
class AfternoonState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

// 晚上状态
class EveningState : public AbstractState
{
public:
    void working(Sanji* sanji) override;
};

源文件 State.cpp

C++

#include <iostream>
#include "State.h"
#include "Sanji.h"
using namespace std;

void ForenoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 8)
    {
        cout << "当前时间<" << time << ">点, 准备早餐, 布鲁克得多喝点牛奶..." << endl;
    }
    else if (time > 8 && time < 11)
    {
        cout << "当前时间<" << time << ">点, 去船头钓鱼, 储备食材..." << endl;
    }
    else
    {
        sanji->setState(new NoonState);
        sanji->working();
    }
}

void NoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 13)
    {
        cout << "当前时间<" << time << ">点, 去厨房做午饭, 给路飞多做点肉..." << endl;
    }
    else
    {
        sanji->setState(new AfternoonState);
        sanji->working();
    }
}

void AfternoonState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 15)
    {
        cout << "当前时间<" << time << ">点, 准备下午茶, 给罗宾和娜美制作爱心甜点..." << endl;
    }
    else if (time > 15 && time < 18)
    {
        cout << "当前时间<" << time << ">点, 和乔巴去船尾钓鱼, 储备食材..." << endl;
    }
    else
    {
        sanji->setState(new EveningState);
        sanji->working();
    }
}

void EveningState::working(Sanji* sanji)
{
    int time = sanji->getClock();
    if (time < 19)
    {
        cout << "当前时间<" << time << ">点, 去厨房做晚饭, 让索隆多喝点汤..." << endl;
    }
    else
    {
        cout << "当前时间<" << time << ">点, 今天过得很高兴, 累了睡觉了..." << endl;
    }
}

在状态类的源文件中包含了山治类的头文件,因为在这些状态类的子类中重写working()函数的时候,需要通过山治类对象调用他的成员函数。通过上面的代码可以看到山治在不同的时间状态下所做的事情是不一样的。

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

2.2 山治

上面定义的一系列的状态都是属于山治这个对象的,只不过通过状态模式来处理山治全天的工作状态变化的时候,把他们都分离出去了,成了独立的个体,从逻辑上讲他们之间是包含和被包含的关系,从UML类图的角度来讲他们之间是组合(整体和部分)关系。关于山治这个类我们可以这样定义:

C++
// Sanji.h
#pragma once
#include "State.h"

class Sanji
{
public:
    Sanji()
    {
        m_state = new ForenoonState;
    }
    void working()
    {
        m_state->working(this);
    }
    void setState(AbstractState* state)
    {
        if (m_state != nullptr)
        {
            delete m_state;
        }
        m_state = state;
    }
    void setClock(int time)
    {
        m_clock = time;
    }
    int getClock()
    {
        return m_clock;
    }
    ~Sanji()
    {
        delete m_state;
    }
private:
    int m_clock = 0;    // 时钟
    AbstractState* m_state = nullptr;
};

在山治类中有两个私有成员变量:

  1. m_clock:通过这整形的时钟变量来描述一天中当前这个时刻的时间点
  2. m_state:通过这个状态指针来保存当前描述山治状态的对象

关于山治类的成员函数有以下这么几个:

  1. working() :工作函数,在不同的时间状态下,工作的内容也不同
  2. setClock() :设置当前的时间
  3. getClock() :得到当前的时间
  4. setState() :设置山治当前的状态

2.3 工作日志

我们可以修改山治类内部的时钟的值来模拟时间的流逝,这样山治的状态就会不停地发生变化了:

C++
int main()
{
    Sanji* sanji = new Sanji;
    // 时间点
    vector<int> data{7, 10, 12, 14, 16, 18, 22};
    for (const auto& item : data)
    {
        sanji->setClock(item);
        sanji->working();
    }
    delete sanji;

    return 0;
}

最后我们来看一下山治这一整天都干了些什么吧:

C++
当前时间<7>点, 准备早餐, 布鲁克得多喝点牛奶...
当前时间<10>点, 去船头钓鱼, 储备食材...
当前时间<12>点, 去厨房做午饭, 给路飞多做点肉...
当前时间<14>点, 准备下午茶, 给罗宾和娜美制作爱心甜点...
当前时间<16>点, 和乔巴去船尾钓鱼, 储备食材...
当前时间<18>点, 去厨房做晚饭, 让索隆多喝点汤...
当前时间<22>点, 今天过得很高兴, 累了睡觉了...

3. 结构图

最后画一下状态模式对应的UML类图(学会状态模式之后,要先画UML类图再写程序。)

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/state/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙

设计模式


9.模板方法模式【和平主义者】

1. 和平主义者

和平主义者是世界顶级科学家贝加庞克为世界政府研制的人形兵器,归海军直属,其相貌和王下七武海中的巴索罗缪·大熊一样,可以发射镭射光,杀伤力和战斗力非常强。海军利用这些和平主义者来消灭赏金上亿的海贼。

两年后,贝加庞克又推出了和平主义者2.0版本,名字叫做炽天使,身材比和平主义者小了很多,但是战斗力和自身携带的技能却上升了一个档次。

对于贝加庞克来说这两款机器人是一脉相承的,也就是说他们的架构是一样的,2.0版本的炽天使只是在原来架构基础上增强了某些功能,或者在原来预留的接口上实现了某些功能,使用这种方式无疑能够使研发效率最大化。

和领先人类科技500年的天才科学家贝加庞克设计机器人的思路类似,在编程的时候也有一种类似的设计模式叫做模板方法模式。模板方法模式就是在基类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。说的再直白一些就是先定义一个基类,在基类中把与需求相关的所有操作函数全部作为虚函数定义出来,然后在这个基类的各个子类中重写父类的虚函数,这样子类基于父类的架构使自己有了和其他兄弟类不一样的行为。模板方法这种设计模式是对多态的典型应用。

模板方法这种模式在显示生活中的应用场景也有很多,比如:

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

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

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

2. 人形兵器

2.1 理想

贝加庞克在设计机器人的时候肯定是花了很多心思,尽量让他趋于完美。所以这款机器人的架构中肯定是留有很多接口的,以方便科学技术成熟之后对他们进行拓展和升级。假设贝加庞克的机器人对应的是一个类,那么在这个类中肯定需要定义很多的虚函数,以方便在子类中进行实现或者是功能的改进,我们认为在这个机器人类中提供了以下这些功能:

  • 机器人的武器系统
  • 机器人的外观
  • 机器人的战斗力
  • 机器人的名字
  • 机器人的自愈能力
  • 机器人是否可以飞行
  • 机器人是否能够自主控制
  • 得到机器人的所有属性

根据以上描述,我们就可以把机器人的抽象类定义出来了:

C++

// 抽象机器人类
class AbstractRobot
{
public:
    // 武器
    virtual void weapon() = 0;
    // 外观
    virtual void appearance() = 0;
    // 战斗能力
    virtual void fightAbility() = 0;
    // 名字
    virtual string getName() = 0;
    // 自愈能力
    virtual void selfHealing() {};
    // 是否能飞
    virtual bool canFlying()
    {
        return false;
    }
    // 是否是自动控制
    virtual bool isAuto()
    {
        return true;
    }
    // 得到机器人属性
    virtual void getProperty()
    {
        cout << "贝加庞克制造的" << getName() << "有以下属性: " << endl;
        if (canFlying())
        {
            cout << "有飞行能力!" << endl;
        }
        else
        {
            cout << "没有飞行能力!" << endl;
        }
        if (isAuto())
        {
            cout << "可以自动控制, 完全体机器人!" << endl;
        }
        else
        {
            cout << "不能自动控制, 半自动机器人!" << endl;
        }
        weapon();
        appearance();
        fightAbility();
        selfHealing();
    }
};

在上面的抽象类中提供了一些纯虚函数,这些纯虚函数在子类中是必须要进行重写的,否则子类也是抽象类,这样子类就无法实例化了。另外,还有一些虚函数,这些虚函数可以根据实际需求可以在子类中重写,也可以不重写。有了这个抽象类,机器人的骨架我们就已经搭建好了,接下来就需要为梦想而奋斗,让梦想照进现实了。

2.2 现实

和平主义者

理想很丰满,现实很骨感,对于贝加庞克这个天才科学家来说在科研的道路上饭也还是得一口一口吃,所以他造出了他理想中的第一代机器人:和平主义者:

C++
// 和平主义者
class Pacifist : public AbstractRobot
{
public:
    // 武器
    void weapon() override
    {
        cout << "可以发射镭射光..." << endl;
    }
    // 外观
    void appearance() override
    {
        cout << "外部和巴索罗米·熊一样, 体型庞大,拥有呈半圆形的耳朵,内部似乎金属。" << endl;
    }
    // 能力
    void fightAbility() override
    {
        cout << "结实抗揍, 可以通过手部或者嘴部发射镭射激光, 可以融化钢铁!!!" << endl;
    }
    string getName() override
    {
        return "和平主义者";
    }
};

作为第一代机器人,和平主义者是不完美的,它只是实现了一些必备功能,比如它没有自愈能力,也不能飞行(没有重写父类的这些虚函数,也就是意味着这些功能还没有实现)。

炽天使

又过了两年,技术更加成熟了,所以就有了第二个版本的炽天使:

C++
// 炽天使
class Seraphim : public AbstractRobot
{
public:
    // 武器
    void weapon() override
    {
        cout << "可以发射镭射激光, 鹰眼外形的炽天使携带者一把巨剑, 可以斩断一切!!!" << endl;
    }
    // 外观
    void appearance() override
    {
        cout << "外观和七武海小时候的外形一样, 并且拥有一对和烬一样的翅膀!!!" << endl;
    }
    // 能力
    void fightAbility() override
    {
        cout << "不仅可以发射镭射激光, 还拥有七武海的能力, 牛逼plus, 无敌了!!!!" << endl;
    }
    // 自愈能力
    void selfHealing() override
    {
        cout << "非常厚实抗揍, 并且拥有非常强的自愈能力, 开挂了!!!" << endl;
    }
    // 是否能飞
    bool canFlying() override
    {
        return true;
    }
    string getName() override
    {
        return "炽天使";
    }
};

可以看到第二个版本的机器人 -- 炽天使现在有了自愈和飞行的能力(重写了父类的这些虚函数,实现了对应的功能)。

通过上面的代码可以看到不管是第一代和平主义者还是第二代的炽天使他们是发生了改变,但这些变化背后对应的却是不变,那就是父类提供的架构没有改变。假设以后贝加庞克要制造第三代的机器人和平天使大丙,只需在新的子类中重新实现父类提供的虚函数就可以了。

2.3 性能

最后我们来对比一下贝加庞克的这两款机器人的属性,看一下炽天使是否可以秒杀和平主义者:

C++
int main()
{
    AbstractRobot* robot = nullptr;
    robot = new Pacifist;
    robot->getProperty();
    delete robot;
    cout << "====================================" << endl;
    robot = new Seraphim; 
    robot->getProperty();
    delete robot;
    return 0;
}

得到的结果如下:

C++
贝加庞克制造的和平主义者有以下属性:
没有飞行能力!
可以自动控制, 完全体机器人!
可以发射镭射光...
外部和巴索罗米·熊一样, 体型庞大,拥有呈半圆形的耳朵,内部似乎金属。
结实抗揍, 可以通过手部或者嘴部发射镭射激光, 可以融化钢铁!!!
====================================
贝加庞克制造的炽天使有以下属性:
有飞行能力!
可以自动控制, 完全体机器人!
可以发射镭射激光, 鹰眼外形的炽天使携带者一把巨剑, 可以斩断一切!!!
外观和七武海小时候的外形一样, 并且拥有一对和烬一样的翅膀!!!
不仅可以发射镭射激光, 还拥有七武海的能力, 牛逼plus, 无敌了!!!!
非常厚实抗揍, 并且拥有非常强的自愈能力, 开挂了!!!

果然是科技改变世界,怪不得世界政府废除了七武海!

3. 结构图

最后将上面的例子对应的UML类图画一下(学会了模板方法模式之后,需要先画UML类图,再写程序。)

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/template-method/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


10.访问者模式[再见,香波地群岛]

1. 遭遇香波地

在海贼世界中,香波地群岛位于伟大航路中间的红土大陆前方。岛屿是由多颗大树构成,地面就是树根,会从地面冒出气泡。在这里,路飞的好朋友人鱼凯米被人贩子拐卖并卖给了天龙人,路飞为了救凯米狠狠地揍了天龙人。最后,草帽团被巴索罗缪·大熊的果实能力拍到了世界各地,草帽团就地解散。

对于当时的草帽团来说一共有九个人,面对这个打不过的敌人大熊,九个人都做了最后的反抗和挣扎,在这个过程中这些人的状态和行为或许相同相互不同,归纳总结一下就是无助、恐惧、愤怒

如果我们想要通过程序复刻上面动态图片中的这个场景是有很多种处理方式的,最简单的一种就是定义一个人的基类,然后让草帽团的各成员作为这个类的子类,在各个子类中来具体描述他们面对大熊的攻击时的反应和状态。

  1. 索隆很愤怒决定要砍了大熊
  2. 山治很愤怒决定要踢死大熊
  3. 乌索普很恐惧在心里画圈圈诅咒大熊
  4. 路飞很愤怒想锤死大熊
  5. 乔巴很愤怒想拍死大熊
  6. 布鲁克很愤怒用已死的身体阻挡大熊
  7. 弗兰奇很愤怒思考怎么弄死大熊
  8. 娜美很恐惧,请求伙伴的帮助
  9. 罗宾很恐惧,请求伙伴的帮助

可以看到这么写有一个弊端,就是比较啰嗦,其实草帽团面对大熊的突如其来的攻击就两个状态:愤怒和恐惧,在这两种状态下的反应就是战斗和求助。所以我们可以把上面的这个场景重构一下:

如果草帽团的某些成员在面对大熊攻击时的状态反应是一样的,那么在这些子类中就会出现很多相同的冗余代码。有一种更好的处理思路就是将状态和人分开,其中草帽团的各个成员我们可以看做是对象,草帽团成员的反应和状态我们可以将其看做是算法,这种将算法与其所作用的对象隔离开来的设计模式就叫做访问者模式,其实就是通过被分离出的算法来访问对应的对象。

关于访问者模式,在日常生活中对应的场景也有很多,比如:

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

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

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

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

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

2. 再见, 草帽团

2.1 草帽团成员

在香波地群岛的时候,草帽团的成员一共有9人,如果使用访问者模式来处理他们在遭遇大熊之后的状态,那么就需要将状态(算法)和人(对象)分离开来,关于这九个成员我们可以按照性别进行划分:男人和女人。不论是哪类成员最终都需要接受对应的被分离出的那个行为状态的访问,我们只需要在这个成员类中提供一个接受访问的函数就可以了,所以这个抽象的成员类定义如下:

C++
// 抽象的成员类
class AbstractMember
{
public:
    AbstractMember(string name) : m_name(name){}
    string getName()
    {
        return m_name;
    }
    // 接受状态对象的访问
    virtual void accept(行为/动作类* action) = 0;
    virtual ~AbstractMember() {}
protected:
    string m_name;
};
  • 这个抽象的基类的构造函数提供了一个参数,用于指定当前成员的名字。

  • 调用getName() 函数可以得到当前成员的名字

  • 调用

    accept()
    

    函数表示当前成员对象接受了行为/动作类的访问

    • 关于行为/动作也可以对应很多种情况,所以此处的类也应该是一个基类
    • 目前行为/动作类还没有被定义,所以先用中文注释代替。

有了上面的抽象基类,按照里面的分类就可以把成员子类定义出来了:

C++
// 男性成员
class MaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(行为/动作* action) override
    {
        // do something
    }
};

// 女性成员
class FemaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(行为/动作* action) override
    {
        // do something
    }
};

关于草帽团成员类,他们的行为全部被分离出去了,所以在当前成员类中剩下的就是这个成员自身的一些属性信息,比如:性别、姓名、在船上的职务、被悬赏的金额等。

如果在成员类中调用了accept()方法,就可以通过参数传入的行为/动作对象调用它的成员函数,这个动作就是当前草帽团某个成员对象被分离出去的动作或者行为,通过这种方式成员类和行为/动作类就可以关联到一起了。

2.2 最后的挣扎

草帽一伙在被大熊拍飞之前都做出了最后的挣扎,他们对应的就是一些状态和行为,假设状态就两种:愤怒和恐惧。这两个状态的所有者就是上面定义的两个类男性成员类和女性成员类。我们先把这两种行为状态对应的基类定义出来:

C++
// Visitor.h
// 类声明
class MaleMember;
class FemaleMember;
// 抽象的动作类
class AbstractAction
{
public:
    // 访问男人
    virtual void maleDoing(MaleMember* male) = 0;
    // 访问女人
    virtual void femalDoing(FemaleMember* female) = 0;
    virtual ~AbstractAction() {}
};

这个类提供了两个虚函数:

  • maleDoing():男性成员的行为函数,所以需要访问一个男性成员的对象,故参数的类型为MaleMember*
  • femalDoing():女性成员的行为函数,所以需要访问一个女性成员的对象,故参数的类型为FemaleMember*
  • 这两个Doing()函数之所以参数是成员类的子类类型是因为男性成员和女性成员对象所拥有的函数可以是不一样的(除了从父类继承,可能在子类内部也会定义一些只属于这个子类的成员函数),这样才更方便进行函数的调用。
  • 在这个类的头文件中本别对MaleMember 类FemaleMember 类进行了声明,但并没有包含他们对应的头文件,目的是防止头文件重复包含。

有了行为类的基类,下面把它对应的两个子类定义出来:

头文件 Visitor.h

C++
// 愤怒
class Anger : public AbstractAction
{
public:
    void maleDoing(MaleMember* male) override;
    void femalDoing(FemaleMember* female) override;
    void warning();
    void fight();
};

// 恐惧
class Horror : public AbstractAction
{
public:
    void maleDoing(MaleMember* male) override;
    void femalDoing(FemaleMember* female) override;
    void help();
    void thinking();
};

源文件 Visitor.cpp

C++

#include <iostream>
#include "Visitor.h"
#include "Member.h"
#include <list>
#include <vector>
using namespace std;

void Anger::maleDoing(MaleMember* male)
{
    cout << "我是草帽海贼团的" << male->getName() << endl;
    fight();
}

void Anger::femalDoing(FemaleMember* female)
{
    cout << "我是草帽海贼团的" << female->getName() << endl;
    warning();
}

void Anger::warning()
{
    cout << "大家块逃,我快顶不住了, 不要管我!!!" << endl;
}

void Anger::fight()
{
    cout << "只要还活着就得跟这家伙血战到底!!!" << endl;
}

void Horror::maleDoing(MaleMember* male)
{
    cout << "我是草帽海贼团的" << male->getName() << endl;
    thinking();
}

void Horror::femalDoing(FemaleMember* female)
{
    cout << "我是草帽海贼团的" << female->getName() << endl;
    help();
}

void Horror::help()
{
    cout << "这个大熊太厉害, 太可怕了, 快救救我。。。" << endl;
}

void Horror::thinking()
{
    cout << "得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!" << endl;
}

在这个两个字行为类中分别通过maleDoing()femalDoing()函数完成了对成员类的访问,通过参数传递进来的成员对象得到了成员属性,然后在行为类中赋予这个成员对象一系列的行为,这样成员对象和成员对象的行为就又被整合到一起了。

行为类被定义出来之后,我们就可以把前面的成员类补充完整了:

C++
// Member.h
#pragma once
#include <iostream>
#include "Visitor.h"
using namespace std;
// 抽象的成员类
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 MaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(AbstractAction* action) override
    {
        action->maleDoing(this);
    }
};

// 女性成员
class FemaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(AbstractAction* action) override
    {
        action->femalDoing(this);
    }
};

在上面的代码中用到了一种双分派技术

  1. 在调用成员类的accept() 函数的时候,将具体地行为状态通过参数传递给了男性成员或者女性成员。
  2. accept() 函数中通过行为状态对象调用行为函数的时候,将当前成员对象传递给了状态对象。

accept() 函数是一个双分派操作,它得到执行的操作不仅取决于传入的状态类的具体状态,还取决于它访问的人的类别。

2.3 集合

草帽一伙要被拍飞了,我们可以把这九个人先聚到一起,所以可以定义一个草帽团类:

C++
// Visitor.cpp
// 草帽团
class CaoMaoTeam
{
public:
    CaoMaoTeam()
    {
        m_actions.push_back(new Anger);
        m_actions.push_back(new Horror);
    }
    void add(AbstractMember* member)
    {
        m_members.push_back(member);
    }
    void remove(AbstractMember* member)
    {
        m_members.remove(member);
    }
    void display()
    {
        for (const auto& item : m_members)
        {
            int index = rand() % 2;
            item->accept(m_actions[index]);
        }
    }
    ~CaoMaoTeam()
    {
        for (const auto& item : m_members)
        {
            delete item;
        }
        for (const auto& item : m_actions)
        {
            delete item;
        }
    }
private:
    list<AbstractMember*> m_members;
    vector<AbstractAction*> m_actions;
};

在这个类中使用了两个容器:

  • list 容器:用于存储草帽团成员
  • vector 容器:用于存储草帽团成员的两种行为状态。

通过这个类的display()函数我们就可以了解到草帽团成员在被大熊拍飞之前的行为状态了。

2.3 解散,再见

草帽团成员已集合完毕,大熊可以下手了:

C++
int main()
{
    srand(time(NULL));
    vector<string> names{
        "路飞", "索隆","山治", "乔巴", "弗兰奇", "乌索普", "布鲁克"
    };
    CaoMaoTeam* caomao = new CaoMaoTeam;
    for (const auto& item : names)
    {
        caomao->add(new MaleMember(item));
    }
    caomao->add(new FemaleMember("娜美"));
    caomao->add(new FemaleMember("罗宾"));
    caomao->display();
    delete caomao;
    return 0;
}

最后看一下,草帽团各成员的反应:

C++
我是草帽海贼团的路飞
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的索隆
得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!
我是草帽海贼团的山治
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的乔巴
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的弗兰奇
得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!
我是草帽海贼团的乌索普
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的布鲁克
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的娜美
这个大熊太厉害, 太可怕了, 快救救我。。。
我是草帽海贼团的罗宾
这个大熊太厉害, 太可怕了, 快救救我。。。

3. 结构图

最后将上面的例子对应的UML类图画一下(学会了访问者模式之后,需要先画UML类图,再写程序。)

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

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

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/visitor/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


一样的,那么在这些子类中就会出现很多相同的冗余代码。有一种更好的处理思路就是将状态和人分开,其中草帽团的各个成员我们可以看做是对象,草帽团成员的反应和状态我们可以将其看做是算法,这种将算法与其所作用的对象隔离开来的设计模式就叫做访问者模式,其实就是通过被分离出的算法来访问对应的对象。

关于访问者模式,在日常生活中对应的场景也有很多,比如:

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

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

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

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

    [外链图片转存中...(img-FGC1Y0Ws-1698671472643)]

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

2. 再见, 草帽团

2.1 草帽团成员

在香波地群岛的时候,草帽团的成员一共有9人,如果使用访问者模式来处理他们在遭遇大熊之后的状态,那么就需要将状态(算法)和人(对象)分离开来,关于这九个成员我们可以按照性别进行划分:男人和女人。不论是哪类成员最终都需要接受对应的被分离出的那个行为状态的访问,我们只需要在这个成员类中提供一个接受访问的函数就可以了,所以这个抽象的成员类定义如下:

C++
// 抽象的成员类
class AbstractMember
{
public:
    AbstractMember(string name) : m_name(name){}
    string getName()
    {
        return m_name;
    }
    // 接受状态对象的访问
    virtual void accept(行为/动作类* action) = 0;
    virtual ~AbstractMember() {}
protected:
    string m_name;
};
  • 这个抽象的基类的构造函数提供了一个参数,用于指定当前成员的名字。

  • 调用getName() 函数可以得到当前成员的名字

  • 调用

    accept()
    

    函数表示当前成员对象接受了行为/动作类的访问

    • 关于行为/动作也可以对应很多种情况,所以此处的类也应该是一个基类
    • 目前行为/动作类还没有被定义,所以先用中文注释代替。

有了上面的抽象基类,按照里面的分类就可以把成员子类定义出来了:

C++
// 男性成员
class MaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(行为/动作* action) override
    {
        // do something
    }
};

// 女性成员
class FemaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(行为/动作* action) override
    {
        // do something
    }
};

关于草帽团成员类,他们的行为全部被分离出去了,所以在当前成员类中剩下的就是这个成员自身的一些属性信息,比如:性别、姓名、在船上的职务、被悬赏的金额等。

如果在成员类中调用了accept()方法,就可以通过参数传入的行为/动作对象调用它的成员函数,这个动作就是当前草帽团某个成员对象被分离出去的动作或者行为,通过这种方式成员类和行为/动作类就可以关联到一起了。

2.2 最后的挣扎

草帽一伙在被大熊拍飞之前都做出了最后的挣扎,他们对应的就是一些状态和行为,假设状态就两种:愤怒和恐惧。这两个状态的所有者就是上面定义的两个类男性成员类和女性成员类。我们先把这两种行为状态对应的基类定义出来:

C++
// Visitor.h
// 类声明
class MaleMember;
class FemaleMember;
// 抽象的动作类
class AbstractAction
{
public:
    // 访问男人
    virtual void maleDoing(MaleMember* male) = 0;
    // 访问女人
    virtual void femalDoing(FemaleMember* female) = 0;
    virtual ~AbstractAction() {}
};

这个类提供了两个虚函数:

  • maleDoing():男性成员的行为函数,所以需要访问一个男性成员的对象,故参数的类型为MaleMember*
  • femalDoing():女性成员的行为函数,所以需要访问一个女性成员的对象,故参数的类型为FemaleMember*
  • 这两个Doing()函数之所以参数是成员类的子类类型是因为男性成员和女性成员对象所拥有的函数可以是不一样的(除了从父类继承,可能在子类内部也会定义一些只属于这个子类的成员函数),这样才更方便进行函数的调用。
  • 在这个类的头文件中本别对MaleMember 类FemaleMember 类进行了声明,但并没有包含他们对应的头文件,目的是防止头文件重复包含。

有了行为类的基类,下面把它对应的两个子类定义出来:

头文件 Visitor.h

C++
// 愤怒
class Anger : public AbstractAction
{
public:
    void maleDoing(MaleMember* male) override;
    void femalDoing(FemaleMember* female) override;
    void warning();
    void fight();
};

// 恐惧
class Horror : public AbstractAction
{
public:
    void maleDoing(MaleMember* male) override;
    void femalDoing(FemaleMember* female) override;
    void help();
    void thinking();
};

源文件 Visitor.cpp

C++

#include <iostream>
#include "Visitor.h"
#include "Member.h"
#include <list>
#include <vector>
using namespace std;

void Anger::maleDoing(MaleMember* male)
{
    cout << "我是草帽海贼团的" << male->getName() << endl;
    fight();
}

void Anger::femalDoing(FemaleMember* female)
{
    cout << "我是草帽海贼团的" << female->getName() << endl;
    warning();
}

void Anger::warning()
{
    cout << "大家块逃,我快顶不住了, 不要管我!!!" << endl;
}

void Anger::fight()
{
    cout << "只要还活着就得跟这家伙血战到底!!!" << endl;
}

void Horror::maleDoing(MaleMember* male)
{
    cout << "我是草帽海贼团的" << male->getName() << endl;
    thinking();
}

void Horror::femalDoing(FemaleMember* female)
{
    cout << "我是草帽海贼团的" << female->getName() << endl;
    help();
}

void Horror::help()
{
    cout << "这个大熊太厉害, 太可怕了, 快救救我。。。" << endl;
}

void Horror::thinking()
{
    cout << "得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!" << endl;
}

在这个两个字行为类中分别通过maleDoing()femalDoing()函数完成了对成员类的访问,通过参数传递进来的成员对象得到了成员属性,然后在行为类中赋予这个成员对象一系列的行为,这样成员对象和成员对象的行为就又被整合到一起了。

行为类被定义出来之后,我们就可以把前面的成员类补充完整了:

C++
// Member.h
#pragma once
#include <iostream>
#include "Visitor.h"
using namespace std;
// 抽象的成员类
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 MaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(AbstractAction* action) override
    {
        action->maleDoing(this);
    }
};

// 女性成员
class FemaleMember : public AbstractMember
{
public:
    AbstractMember::AbstractMember;
    void accept(AbstractAction* action) override
    {
        action->femalDoing(this);
    }
};

在上面的代码中用到了一种双分派技术

  1. 在调用成员类的accept() 函数的时候,将具体地行为状态通过参数传递给了男性成员或者女性成员。
  2. accept() 函数中通过行为状态对象调用行为函数的时候,将当前成员对象传递给了状态对象。

accept() 函数是一个双分派操作,它得到执行的操作不仅取决于传入的状态类的具体状态,还取决于它访问的人的类别。

2.3 集合

草帽一伙要被拍飞了,我们可以把这九个人先聚到一起,所以可以定义一个草帽团类:

C++
// Visitor.cpp
// 草帽团
class CaoMaoTeam
{
public:
    CaoMaoTeam()
    {
        m_actions.push_back(new Anger);
        m_actions.push_back(new Horror);
    }
    void add(AbstractMember* member)
    {
        m_members.push_back(member);
    }
    void remove(AbstractMember* member)
    {
        m_members.remove(member);
    }
    void display()
    {
        for (const auto& item : m_members)
        {
            int index = rand() % 2;
            item->accept(m_actions[index]);
        }
    }
    ~CaoMaoTeam()
    {
        for (const auto& item : m_members)
        {
            delete item;
        }
        for (const auto& item : m_actions)
        {
            delete item;
        }
    }
private:
    list<AbstractMember*> m_members;
    vector<AbstractAction*> m_actions;
};

在这个类中使用了两个容器:

  • list 容器:用于存储草帽团成员
  • vector 容器:用于存储草帽团成员的两种行为状态。

通过这个类的display()函数我们就可以了解到草帽团成员在被大熊拍飞之前的行为状态了。

2.3 解散,再见

草帽团成员已集合完毕,大熊可以下手了:

C++
int main()
{
    srand(time(NULL));
    vector<string> names{
        "路飞", "索隆","山治", "乔巴", "弗兰奇", "乌索普", "布鲁克"
    };
    CaoMaoTeam* caomao = new CaoMaoTeam;
    for (const auto& item : names)
    {
        caomao->add(new MaleMember(item));
    }
    caomao->add(new FemaleMember("娜美"));
    caomao->add(new FemaleMember("罗宾"));
    caomao->display();
    delete caomao;
    return 0;
}

最后看一下,草帽团各成员的反应:

C++
我是草帽海贼团的路飞
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的索隆
得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!
我是草帽海贼团的山治
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的乔巴
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的弗兰奇
得辅助同伴们一块攻击这个家伙, 不然根本打不过呀!!!
我是草帽海贼团的乌索普
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的布鲁克
只要还活着就得跟这家伙血战到底!!!
我是草帽海贼团的娜美
这个大熊太厉害, 太可怕了, 快救救我。。。
我是草帽海贼团的罗宾
这个大熊太厉害, 太可怕了, 快救救我。。。

3. 结构图

最后将上面的例子对应的UML类图画一下(学会了访问者模式之后,需要先画UML类图,再写程序。)

[外链图片转存中...(img-2NU6ovbZ-1698671472643)]

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

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

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

文章作者: 苏丙榅

文章链接: https://subingwen.cn/design-patterns/visitor/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙


相关推荐
思忖小下6 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
liyinuo20179 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范
aaasssdddd9611 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
T1an-111 小时前
设计模式之【观察者模式】
观察者模式·设计模式
思忖小下13 小时前
梳理你的思路(从OOP到架构设计)_设计模式Factory Method模式
设计模式·工厂方法模式·eit
霁月风14 小时前
设计模式——工厂方法模式
c++·设计模式·工厂方法模式
发飙的蜗牛'16 小时前
23种设计模式
android·java·设计模式
NorthCastle1 天前
设计模式-创建型模式-简单工厂模式详解
设计模式·简单工厂模式
越甲八千1 天前
重拾设计模式-外观模式和适配器模式的异同
设计模式·适配器模式·外观模式
越甲八千1 天前
重拾设计模式--适配器模式
设计模式·适配器模式