通辽建设网站,电商网站建设的内容,邯郸旅游,电商网站运营策划1.前言
1.CUML类图
面向对象设计主要就是使用UML的类图#xff0c;类图用于描述系统中所包含的类以及它们之间的相互关系#xff0c;帮助人们简化对系统的理解#xff0c;它是系统分析和设计阶段的重要产物#xff0c;也是系统编码和测试的重要模型依据。下面基于C这门语…1.前言
1.CUML类图
面向对象设计主要就是使用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和两个子类Cuckoo、Eagle
Cclass 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()是虚函数它有两个子类Cuckoo和Eagle在这两个子类中重写了父类的虚函数在使用带空心三角的实现表示继承关系的时候有空心三角的一端指向父类另一端连接子类。
关联关系
关联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类图为 依赖关系通常通过三种方式来实现 将一个类的对象作为另一个类中方法的参数 在一个类的方法中将另一个类的对象作为其对象的局部变量 在一个类的方法中调用另一个类的静态方法 类之间的关系强弱顺序是这样的继承泛化 组合 聚合 关联 依赖。
关联关系、聚合关系、组合关系之间的区别
从上文可以看出关联关系、聚合关系和组合关系三者之间比较相似最后就来总结一下这三者之间的区别
关联和聚合的区别主要在于语义上关联的两个对象之间一般是平等的聚合则一般是不平等的。聚合和组合的区别则在语义和实现上都有差别 组合的两个对象之间生命周期有很大的关联被组合的对象在组合对象创建的同时或者创建之后创建在组合对象销毁之前销毁聚合则无需考虑这些事情。一般来说被组合对象不能脱离组合对象独立存在而且也只能属于一个组合对象聚合则不一样被聚合的对象可以属于多个聚合对象。
最后再举例子来描述一下这三种关系
朋友之间属于关联关系因为这种关系是平等的关联关系只是用于表示两个对象之间的一种简单的联系而已。图书馆看书的时候人和书属于聚合关系。书是可以独立存在的而且书不仅可以属于自己也可以属于别人。人和自己的心脏属于组合关系因为心脏不能脱离人体而独自存在。
不过实际应用中这三种关系的界限划分其实没有那么清楚有些时候我们会感觉组合和聚合没什么区别所以在设计的时候没必要死抠细节只要能够利用对象之间的关系设计出可行的解决方案即可。
文章作者: 苏丙榅
文章链接: https://subingwen.cn/design-patterns/UML-class-diagrams/
版权声明: 本博客所有文章除特别声明外均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙
设计模式 2.设计模式三原则
我们在进行程序设计的时候要尽可能地保证程序的可扩展性、可维护性和可读性所以需要使用一些设计模式这些设计模式都遵循了以下三个原则下面来依次为大家介绍。
单一职责原则
C面向对象三大特性之一的封装指的就是将单一事物抽象出来组合成一个类所以我们在设计类的时候每个类中处理的是单一事物而不是某些事物的集合。
设计模式中所谓的单一职责原则就是对一个类而言应该仅有一个引起它变化的原因其实就是将这个类所承担的职责单一化就跟海贼王中的能力者一样每个人只能吃一颗恶魔果实拥有某一种能力【黑胡子这个Bug除外】。
如果一个类承担的职责过多就等于把这些职责耦合到了一起一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致设计变得脆弱当变化发生时设计会遭受到意想不到的破坏。 偷袭白胡子的这个男人被白胡子视为自己的儿子他叫斯库亚德本来是一起去救艾斯的【此时的他是一个单一职责的类】后来被赤犬挑拨离间想到了自己的过去并萌生了别的想法【这个类被追加了一些其他的职责】最终背叛并刺穿了白胡子的身体【这个类没能完成开始时的预期任务就此废掉了】。由此可见让一个类承担太多的职责绝非好事。
软件设计真正要做的事情就是发现根据需求发现职责并把这些职责进行分离添加新的类给当前类减负越是这样项目才越容易维护。
开放封闭原则
开放 – 封闭原则说的是软件实体类、模块、函数等可以扩展但是不可以修改。也就是说对于扩展是开放的对于修改是封闭的。
该原则是程序设计的一种理想模式在很多情况下无法做到完全的封闭。但是作为设计人员应该能够对自己设计的模块在哪些位置产生何种变化了然于胸因此需要在这些位置创建抽象类来隔离以后发生的这些同类变化其实就是对多态的应用创建新的子类并重写父类虚函数用以更新处理动作。
此处的抽象类其实并不等价与C中完全意义上是抽象类需要有纯虚函数这里所说的抽象类只需要包含虚函数纯虚函或非纯虚函数能够实现多态即可。 草帽团船长路飞从出海到现在一共召集了9个伙伴这些伙伴在船上的职责都是不一样的有音乐家、船工、舵手、航海士、剑士、考古学家、狙击手、厨师、船医作为船长没有要求自己学习这些船员的技能【对自己来说是封闭的】而是提出了伙伴的概念【这就是一个可变的抽象】最终找到了优秀的伙伴加入【对外是开放的每个伙伴都是这个抽象的具体实现但他们的技能又有所不同】事实证明这样做是对的如果反其道而行之不仅违背了开放封闭原则也违背了单一职责原则。
开放 – 封闭原则是面向对象设计的核心所在这样可以给我们设计出的程序带来巨大的好处使其可维护性、可扩展性、可复用性、灵活性更好。
依赖倒转原则
关于依赖倒转原则对应的是两条非常抽象的描述
高层模块不应该依赖低层模块两个都应该依赖抽象。抽象不应该依赖细节细节应该依赖抽象。
先用人话解释一下这两句话中的一些抽象概念
高层模块可以理解为上层应用就是业务层的实现低层模块可以理解为底层接口比如封装好的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. 独生子女
如果使用单例模式首先要保证这个类的实例有且仅有一个也就是说这个对象是独生子女如果我们实施计划生育只生一个孩子不需要也不能给再他增加兄弟姐妹。因此就必须采取一系列的防护措施。对于类来说以上描述同样适用。涉及一个类多对象操作的函数有以下几个
构造函数创建一个新的对象拷贝构造函数根据已有对象拷贝出一个新的对象拷贝赋值操作符重载函数两个对象之间的赋值
为了把一个类可以实例化多个对象的路堵死可以做如下处理
构造函数私有化在类内部只调用一次这个是可控的。 由于使用者在类外部不能使用构造函数所以在类内部创建的这个唯一的对象必须是静态的这样就可以通过类名来访问了为了不破坏类的封装我们都会把这个静态对象的访问权限设置为私有的。在类中只有它的静态成员函数才能访问其静态成员变量所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象。 拷贝构造函数私有化或者禁用使用 delete拷贝赋值操作符重载函数私有化或者禁用从单例的语义上讲这个函数已经毫无意义所以在类中不再提供这样一个函数故将它也一并处理一下。
由于单例模式就是给类创建一个唯一的实例对象所以它的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 使用了一个没有被初始化的队列对象就出问题了出现这种情况是概率问题需要反复的大量测试问题才可能会出现。
在C11中引入了原子变量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(); // 加锁: 方式1lock_guardmutex locker(m_mutex); // 加锁: 方式2queue 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 atomicTaskQueue* m_taskQ;static mutex m_mutex;
};
atomicTaskQueue* TaskQueue::m_taskQ;
mutex TaskQueue::m_mutex;int main()
{TaskQueue* queue TaskQueue::getInstance();queue-print();return 0;
}上面代码中使用原子变量atomic的store() 方法来存储单例对象使用load() 方法来加载单例对象。在原子变量中这两个函数在处理指令的时候默认的原子顺序是memory_order_seq_cst顺序原子操作 - sequentially consistent使用顺序约束原子操作库整个函数执行都将保证顺序执行并且不会出现数据竞态data races不足之处就是使用这种方法实现的懒汉模式的单例执行效率更低一些。
静态局部对象
在实现懒汉模式的单例的时候相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题那就是使用静态局部局部对象对应的代码实现如下
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 行定义了一个静态局部队列对象并且将这个对象作为了唯一的单例实例。使用这种方式之所以是线程安全的是因为在C11标准中有如下规定并且这个操作是在编译时由编译器保证的 如果指令逻辑进入一个未被初始化的声明变量所有并发执行应当等待该变量完成初始化。 最后总结一下懒汉模式和饿汉模式的区别
懒汉模式的缺点是在创建实例对象的时候有安全问题但这样可以减少内存的浪费如果用不到就不去申请内存了。饿汉模式则相反在我们不需要这个实例对象的时候它已经被创建出来占用了一块内存。对于现在的计算机而言内存容量都是足够大的这个缺陷可以被无视。
5. 替巴基写一个任务队列
作为程序猿的我们如果想给巴基的账本升级成一个应用程序首要任务就是设计一个单例模式的任务队列那么就需要赋予这个类一些属性和方法
属性 存储任务的容器这个容器可以选择使用STL中的队列queue)互斥锁多线程访问的时候用于保护任务队列中的数据 方法主要是对任务队列中的任务进行操作 任务队列中任务是否为空往任务队列中添加一个任务从任务队列中取出一个任务从任务队列中删除一个任务
根据分析就可以把这个饿汉模式的任务队列的单例类定义出来了
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_guardmutex locker(m_mutex);bool flag m_taskQ.empty();return flag;}// 添加任务void addTask(int data){lock_guardmutex locker(m_mutex);m_taskQ.push(data);}// 取出一个任务int takeTask(){lock_guardmutex locker(m_mutex);if (!m_taskQ.empty()){return m_taskQ.front();}return -1;}// 删除一个任务bool popTask(){lock_guardmutex locker(m_mutex);if (!m_taskQ.empty()){m_taskQ.pop();return true;}return false;}
private:TaskQueue() default;static TaskQueue m_obj;queueint 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. 工厂模式的特点
在海贼王中作为原王下七武海之一的多弗朗明哥可以说是新世界最大的流氓头子拥有无上的权利和无尽的财富。他既是德雷斯罗萨国王又是地下世界的中介控制着世界各地的诸多产业人造恶魔果实工厂就是其中之一。 人造恶魔果实的最大买家是四皇之一的凯多凯多其实很明智他没有自己去生产可能有这么几个因素
凯多手下没有像凯撒·库朗一样的科学家无法掌握生产人造恶魔果实这种顶级的科学技术【意味着构造一个对象有时候需要经历一个非常复杂的操作流程既然麻烦那索性就不干了】。有需求下单就行只需关心结果无需关心过程【实现了解耦合】。人造恶魔果实出了问题自己无责任售后直接找明哥【便于维护】。
在我们现实生活中也是一样买馒头和自己蒸馒头、去饭店点一份大盘鸡和自己养鸡杀鸡做大盘鸡这是全然不同的两种体验
自己做麻烦而且有失败的风险需要自己承担后果。买现成的可以忽略制作细节方便快捷并且无风险得到的肯定是美味的食物。
对于后者就相当于是一个加工厂通过这个工厂我们就可以得到想要的东西在程序设计中这种模式就叫做工厂模式工厂生成出的产品就是某个类的实例也就是对象。关于工厂模式一共有三种分别是简单工厂模式、工厂模式、抽象工厂模式。
通过上面人造恶魔果实的例子我们能够了解到不论使用哪种工厂模式其主要目的都是实现类与类之间的解耦合这样我们在创建对象的时候就变成了拿来主义使程序更加便于维护。在本节中先介绍简单工厂模式。
基于简单工厂模式去创建对象的时候需要提供一个工厂类专门用于生产需要的对象这样关于对象的创建操作就被剥离出去了。
简单工厂模式相关类的创建和使用步骤如下
创建一个新的类, 可以将这个类称之为工厂类。对于简单工厂模式来说需要的工厂类只有一个。在这个工厂类中添加一个公共的成员函数通过这个函数来创建我们需要的对象关于这个函数一般将其称之为工厂函数。关于使用首先创建一个工厂类对象然后通过这个对象调用工厂函数这样就可以生产出一个指定类型的实例对象了。
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;
}关于恶魔果实的类型上面的类中用到了强类型枚举C11新特性增强了代码的可读性并且将枚举元素设置为了char类型节省了内存。函数createSmile(Type type)的返回值是void*类型这样处理主要是因为每个case 语句创建出的对象类型是不一样的为了实现兼容故此这样处理。得到函数createSmile(Type type)的返回值之后还需要将其转换成实际的类型处理起来还是比较繁琐的。
关于工厂函数的返回值在C中还有一种更好的解决方案就是使用多态。如果想要实现多态需要满足三个条件
类和类之间有继承关系。父类中有虚函数并且在子类中需要重写这些虚函数。使用父类指针或引用指向子类对象。
所以我们需要给人造恶魔果实提供一个基类然后让上边的三个类SheepSmile、LionSmile、BatSmile作为子类继承这个基类。根据分析我们就有画出简单工厂模式的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;
}通过上面的代码我们实现了一个简单工厂模式关于里边的细节有以下几点需要说明 由于人造恶魔果实类有继承关系 并且实现了多态所以父类的析构函数也应该是虚函数这样才能够通过父类指针或引用析构子类的对象。 工厂函数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 、Weapon、Engine有依赖关系在工厂函数中创建了它们的实例对象和Ship 类有依赖关系在工厂函数中创建了它的实例对象
关于Ship类它可以和ShipBody 、Weapon、Engine可以是聚合关系也可以是组合关系
组合关系析构Ship类对象的时候也释放了ShipBody 、Weapon、Engine对象聚合关系析构Ship类对象的时候没有释放ShipBody 、Weapon、Engine对象
在上面的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()函数中要通过工厂类的工厂函数生产什么型号的战船和用户的需求息息相关所以这个选择也是用户通过客户端的操作界面做出的在这个例子中关于客户端的界面操作就直接忽略了。
抽象工厂模式适用于比较复杂的多变的业务场景总体上就是给一系列功能相同但是属性会发生变化的组件如船体材料、武器系统、动力系统添加一个抽象类这样就可以非常方便地进行后续的拓展再搭配工厂类就可以创建出我们需要的对象了。
关于简单工厂模式、工厂模式和抽象工厂模式的区别可以做如下总结
简单工厂模式不能遵守开放-封闭原则工厂和抽象工厂模式可以简单工厂模式只有一个工厂类工厂和抽象工厂有多个工厂类工厂模式创建的产品对象相对简单抽象工厂模式创建的产品对象相对复杂 工厂模式创建的对象对应的类不需要提供抽象类【这产品类组件中没有可变因素】抽象工厂模式创建的对象对应的类有抽象的基类【这个产品类组件中有可变因素】
文章作者: 苏丙榅
文章链接: 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:vectorstring 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:mapstring, 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是草帽海贼团的船医它本来是一头驯鹿但是误食了动物系·人人果实之后可以变成人的形态。 乔巴吃了恶魔果实之后的战斗力暂且抛开不谈说说它掌握的第二技能语言此时的他既能听懂人的语言又能听懂动物语言妥妥的语言学家。
人和动物本来无法直接交流但是有了乔巴的存在就相当于了有了一条纽带一座桥梁使得二者之间能够顺畅的沟通。在这里边乔巴充当的就是一个适配器将一个类的接口转换成用户希望的另一个接口使不兼容的对象能够相互配合并一起工作这种模式就叫适配器模式。说白了适配器模式就相当于找了一个翻译。
需要适配器的例子或者场景很多随便列举几个
STL标准模板库有六大组件其中之一的就是适配器。 六大组件分别是容器、算法、迭代器、仿函数、适配器、空间适配器。适配器又可以分为容器适配器、函数适配器、迭代器适配器 台湾省的电压是110V大陆是220V如果他们把大陆的电器带回台湾就需要适配器进行电压的转换。香港的插座插孔是欧式的从大陆去香港旅游就需要带转换头适配器儿媳妇儿和婆婆打架就需要儿子从中调解此时儿子就适配器。手机、平板、电脑等需要的电压并不是220V也需要适配器进行转换。
2. 斜杠型人才
所谓的斜杠型人才就是多才多艺适配器也一样如果它能给多个不相干的对象进行相互之间的适配这个适配器就是斜杠适配器。
还是拿乔巴举例子他既能把人的语言翻译给动物又能把动物的语言翻译给人那么此时的乔巴就是一个双向适配器。
2.1 国宝的血泪史
覆巢之下无完卵清朝末年作为曾经蚩尤的坐骑而今呆萌可爱的国宝大熊猫惨遭西方国家围猎和杀戮其中以美利坚尤甚。
1869年春天法国传教士阿尔芒·戴维在四川宝兴寻找珍稀物种的时候发现了大熊猫国宝的厄运就此开始。
这是美国总统罗斯福两个儿子制作的大熊猫标本 那个时候的旧中国对熊猫还没有一个确切的认识并没有采取任何措施于是外国人对熊猫的猎杀活动更变本加厉起来。据统计在1936年到1946年之间超过16只活体大熊猫从中国运出而熊猫标本更是多达70余具 一切终成过去作为一个程序猿我也不能改变什么但是我决定要写个程序让这群混蛋给被他们杀死的国宝道歉
2.2 抽丝剥茧
对于杀害大熊猫的这群西方的混蛋不论他们怎么忏悔大熊猫肯定也是听不懂的所以需要适配器来完成这二者之间的交流。但是这帮人来自不同的国家它们都有自己的语言所以这个适配器翻译的不是一种人类语言而是多种因此我们要做如下的处理
忏悔的人说的是不同的语言所以需要一个抽象类。适配器需要翻译不同国家的语言所以适配器也需要一个抽象类。西方人需要给大熊猫道歉因此需要给大熊猫定义一个类。
西方罪人
先把西方这群烂人对应的类定义出来
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类图中将抽象的乔巴类抽象适配器类和熊猫类设置为了关联关系除了使用这种方式我们还可以让抽象的适配器类继承熊猫类这样在适配器类中就可以直接使用熊猫类中定义的方法了如下图 上图对应的代码如下
Cclass 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
将海贼团的船员和海贼团之间的继承关系改为聚合关系如下图 盖棺定论
上面的两种方案你觉得哪个更合理一些呢很明显是第二种。
第一种解决方案 每个船员都是当前海贼团的子类这样船员就继承了海贼团的属性合情合理。如果当前海贼团添加了一个成员就需要给当前海贼团类添加一个子类。拿路飞举个例子他在德雷斯罗萨王国打败多弗朗明哥之后组成了草帽大船团小弟一下子扩充了5640人难道要给草帽团添加五千多个子类吗如果这样处理海贼船和船员的耦合度就比较高了。 第二种解决方案 海贼团之间是继承关系但是此时的海贼团也只是一个抽象因为组成海贼团的人已经被抽离了船员已经和所属的海贼团没有了继承关系。关于海贼世界的船员在船上对应不同的职责担任不同的职务他们是一个团队所以可以给船员抽象出一个团队类用于管理船上的成员。抽象的海贼团只有一个空壳子所以要赋予其灵魂也就是给它添加船员此时的海贼团和船员团队可以通过聚合的方式组合成为一个整体。这种解决方案不仅适用于管理海贼团用于管理海军的各个舰队也是没有问题的。
通过上述分析肯定是使用第二种解决方案程序猿的工作量会更少一些且更容易维护和扩展。第二种方式的原则就是将抽象部分和它的实现部分分离使它们可以独立的变化这种处理模式就是桥接模式。
关于桥接模式的使用对应的应用场景也有很多比如
空调、电视机等和它们对应的遥控器 空调、电视机是抽象遥控器是实现 手机品牌和手机软件 手机品牌是抽象手机软件是实现 跨平台的GUI在不同平台上运行 程序的GUI层是抽象操作系统的API是实现
2. 路飞出海
年幼的路飞误食了红发香克斯的橡胶果实人人果实·幻兽种·尼卡形态受到香克斯的影响决定出海他要做的第一件事儿就是找一艘船并找一些伙伴一起去冒险下面我们通过桥接模式来记录下草帽团的信息。
2.1 伙伴
不论是哪艘船上的船员肯定都是有一些个人的身份信息为了将这些信息记录下来先定一个存储数据的结构体
C
// 人员信息
struct Person
{Person(string name, string job, string ability, string reward, string biezhustring()){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();mapstring, 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为船长分别是
俊美海贼团75人巴托俱乐部56人八宝水军1000人艾迪欧海贼团4人咚塔塔海贼团200人巨兵海贼团5人约塔玛利亚大船团4300人
小弟数量总计5640人。 对于草帽大船团的结构组成很像一棵树路飞是这棵树的根节点旗下的七个船长是路飞的子节点。在这七个船长的旗下可能还有若干个船长。。。 像草帽大船团这样能将多个对象组成一个树状结构用以描述部分—整体的层次关系使得用户对单个对象和组合对象的使用具有一致性这样的结构性设计模式叫做组合模式。
现实生活中能够和组合模式对应的场景也有很多下面举例说明
Linux 的树状目录结构国家的行政区划分省级、地级、县级、乡级解放军编制军、师、旅、团、营、连、排、班公司的组织结构树状
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;
};草帽大船团中有若干个番队这个抽象类对应的就是以船为单位的一个团队一艘船就是一个节点它内部定义了如下方法:
设置和获得当前船队的名字 设置名字构造函数获得名字getName() 设置和得到当前船队节点的父节点 设置父节点setParent(AbstractTeam* node)得到父节点getParent() 给当前番队添加一个子船队节点add(AbstractTeam* node)跟当前番队删除一个子船队节点remove(AbstractTeam* node)当前番队和敌人战斗fight()显示当前番队的信息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;}listAbstractTeam* 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:listAbstractTeam* 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_castManagerTeam*(root);listAbstractTeam* children team-getChildren();for (const auto item : children){gameover(item);}}delete root;
}// 和黑胡子战斗
void fighting()
{vectorstring 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中的组合关系在AbstractTeam和ManagerTeam之间画了两条线 继承关系对节点的操作使用的是抽象类中提供的接口以保证操作的一致性 聚合关系 ManagerTeam类型的节点还可以有子节点父节点和子节点的之间的关系需要具体问题具体分析 子节点跟随父节点一起销毁二者就是组合关系UML中的组合关系子节点不跟随父节点一起销毁二者就是聚合关系上面的程序中在父节点的析构函数中没有销毁它管理的子节点所以在上图中标记的是聚合关系
文章作者: 苏丙榅
文章链接: https://subingwen.cn/design-patterns/composite/
版权声明: 本博客所有文章除特别声明外均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙
设计模式 4.装饰模式【黑胡子】
1. 马歇尔·D·蒂奇
在海贼世界中马歇尔·D·蒂奇绰号黑胡子他是黑胡子海贼团的提督、新世界四皇之一自然系暗暗果实和超人系震震果实能力者据说他还可以吃下第三个恶魔果实就目前剧情而言尚未盖棺定论。
对于黑胡子来说它拥有的恶魔果实能力并不是与生俱来的也就是后天获得恶魔果实能力是对黑胡子原有实力的加成。黑胡子获得的这种恶魔果实能力和设计模式中的装饰模式差不多都是动态的给一个对象绑定额外的属性比如能力、职责、功能等。
关于装饰模式也可以称之为封装模式所谓的封装就是在原有行为之上进行拓展并不会改变该行为看下面的例子
在进行网络通信的时候数据是基于IOS七层或四层网络模型某些层合并之后就是四层模型进行传输通过下图可得知从应用层到物理层数据每向下走一层就会被封装一层最后将封装好的数据以比特流的方式发送给接收端。封装之后数据只是变得更复杂了, 并没有改变它是数据的本质。 A端和B端进行网络通信默认数据是在网络环境中裸奔的如果想要对数据进行装饰也就是封装就可以在发送数据之前对其加密接收端收到数据之后对其解密。加解密是对数据的装饰但是没有改变数据的本质。平时都是穿长衣长裤疫情来袭之后穿上了防护服。防护服是对人的装饰没有改变本体是人的本质。
上述例子中都是对原有行为进行了拓展但是并没有改变原有行为就好比饿了去煮面条为了使其更美味最终会对其进行装饰做成打卤面、炸酱面、热干面、油泼面等不论怎么处理最终得到的还是面条最终不可能得到一锅酱牛肉大方向是不会变的。
2. 解构黑胡子
从概念上对装饰模式有了一定的了解之后继续分析黑胡子的个人战斗力根据文章开头对他的介绍可以知道他的能力来自三个不同的方向
与生俱来加上后天努力练就的本领来自自然系·暗暗果实的能力来自超人系·震震果实的能力
所以这两个恶魔果实的能力就是对黑胡子个人战力的装饰加成。另外还需要明确一点对于恶魔果实来说不是只有黑胡子能吃谁吃了都会拥有对应的果实能力这样这个人的战斗力也就提升了。
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之后还是一个抽象类关于这个类有以下几点需要说明
在DevilFruit 类中没有重写父类Soldier的纯虚函数fight()所以它还是抽象类恶魔果实有很多种类每种恶魔果实能力不同所以战斗方式也不同因此需要在恶魔果实的子类中根据每种果实能力去重写作战函数fight()的行为。恶魔果实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;}
};关于这两个恶魔果实子类需要说明以下几点:
在重写父类的fight()函数的时候用当前恶魔果实能力和战士的自身能力进行了加成调用了战士对象的作战函数 m_human-fight()在原有基础上提升了其战斗力。在两个恶魔果实子类中可以根据实际需要定义类独有的方法比如DarkFruit 类中有 warning() 方法QuakeFruit 类中却没有。再次强调这两个子类都继承了父类的附魔函数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. 开炮
桑尼号是草帽一伙的第二艘海贼船设计者是弗兰奇使用的材料是价值不菲、世界上最强的巨树“亚当”。主要特色是狮子造形的船头及“士兵船坞系统”此外还有草皮做成的甲板附有大型水族馆的房间、图书馆等许多方便的设备。
桑尼号船头的狮吼炮是一个非常厉害的武器。它能够从狮嘴发射出威力超强的加农炮可利用狮头内部操控室的“狙击圈”自由网罗目标。要发动狮吼炮必须同时使用两桶可乐的能量‘风来喷射’才可以避免后座力让船飞退。
想要发射狮吼炮操作很简单瞄准目标拉动拉杆就可以发射了。但是看似简单的加农炮发射其底层是需要很多个系统协同配合才能完成的
可乐注入系统能量转换系统将注入的可乐转换成能量瞄准系统目标锁定系统加农炮控制系统风来炮稳定系统抵消后坐力让船体稳定。 这么复杂的系统对于使用者来说其实就是一个按钮加一个拉杆。不论狮吼炮的设计者弗兰奇有没有学过设计模式但他确实用到了一种设计模式外观模式。外观模式就是给很多复杂的子系统提供一个简单的上层接口并在这些接口中包含用户真正关心的功能。
关于外观模式的应用在实际生活中也有有很多的场景
通过电商平台下单就可以收到自己需要的商品。 上层接口用于下单、查看物流信息、确认收货的用户界面底层模块供货商、仓库、包装、送货、支付处理、售后等 购买基金 上层接口可供用户操作的UI界面底层模块基金分类、购买股票、购买国债、购买企业债 音频转换工具 上层接口可供用户操作的UI界面底层模块MP3编解码模块、FALC编解码模块、APE编解码模块、等。。。 智能家居系统 上层接口可供用户操作的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·卡普也被称为“铁拳卡普”是海军中的传奇人物相传卡普数次将海贼王罗杰逼入绝境被誉为“海军英雄”。卡普是路飞的爷爷经常将霸气缠绕在拳头上来打艾斯与路飞的头因为攻击中充满了爱所以名为“爱之铁拳”每次都会在路飞头上留下包。 虽然自己是海军但是儿子却是革命军孙子是海贼。拳骨陨石·流星群是卡普的战斗招式之一即连续不断地向敌人投掷炮弹其效果就如流星砸向敌人一般。卡普曾以此招在水之七都向路飞告别我们来回顾一下当时的场景 关于卡普老爷子的实力是毋庸置疑的假设我们现在是负责游戏开发的程序猿要复刻这段场景其中出现频率最高的就是炮弹。这里有一个很现实的亟待解决的问题内存的消耗问题。
每个炮弹都是一个对象每个对象都会占用一块内存炮弹越多占用的内存就越大如果炮弹足够多可能会出现内存枯竭问题假设内存足够大频繁的创建炮弹对象会影响游戏的流畅度性能低
关于游戏中的炮弹应该有以下一些需要处理的属性
炮弹的坐标炮弹的速度炮弹的颜色渲染炮弹的精灵图就是一张大图上有很多小的图片通过进行位置的控制从大图中取出想要的某一张小的图片 上面就是一张关于炮弹爆炸过程的精灵图片假设一张炮弹的精灵图片有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 类中添加了对应的get和set方法。
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:mapstring, SharedBombBody* m_bodyMap;
};在享元工厂内部有一个map 容器用于存储各种型号的炮弹的享元数据这个享元工厂就相当于一个对象池当调用了getSharedData(string name)函数之后如果能够从map 容器找到name对应的享元对象就返回该对象如果找不到就创建一个新的享元对象并储存起来这样就可以实现对象的复用了。
2.5 发射炮弹
最后就可以把炮弹制作出来并让其在游戏中按照指定的轨迹运动了:
C
int main()
{// 发射炮弹BombBodyFactory* factory new BombBodyFactory;vectorLaunchBomb* ballList;vectorstring 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:mapstring, SharedBombBody* m_bodyMap;
};int main()
{// 发射炮弹BombBodyFactory* factory new BombBodyFactory;vectorLaunchBomb* ballList;vectorstring 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类图中并没有画出投掷炸弹的这个类。除此之后还有几个知识点需要我们做到心中有数
享元模式中的享元类可以有子类也可以没有享元模式中可以添加享元工厂也可以不添加享元工厂的作用和单例模式类似但二者的关注点略有不同 单例模式关注的是类的对象有且只有一个享元工厂关注的是某个实例对象是否可以共享
文章作者: 苏丙榅
文章链接: https://subingwen.cn/design-patterns/flyweight/
版权声明: 本博客所有文章除特别声明外均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 爱编程的大丙 7.代理模式【电话虫】
1. 电话虫
在海贼中有一种神奇的通信工具叫做电话虫Den Den Mushi外形如蜗牛身上带有斑点或条纹或通体纯色壳顶上有对讲机或按键不接通时会睡觉接通时会惊醒并发出“波噜波噜”的声音在通话时电话虫的嘴巴会如同讲话人的嘴巴一样动也有人的感情听得懂人类说话工作原理是将人的声音转化为电话虫的声音进行长距离传接经作者尾田荣一郎在SBS上证实这是自然生长的一种虫。 如果拥有了属于对方的电话虫不论彼此相隔有多远都可以进行时时通信通过电话虫除了可以听到对方的声音还能看到对方的表情妥妥的一个代理人。在设计模式中有一种模式叫做代理模式代理模式和电话虫差不多都是为其他对象提供一种代理以控制对这个对象的访问。
生活中关于代理的例子也有很多比如
通过信用卡、微信、支付宝等代替现金支付开发一套对接数据库服务器的接口并提供给客户使用用于提高服务器的访问效率跑腿小哥代替大聪明给异地的女盆友送花。通过VPN架梯子访问外网。
2. 解构电话虫
如果我们想要用代理模式来描述一下电话虫的行为里边有如下几个细节
说话的人是一个对象电话虫也是一个对象电话虫模拟的是说话的人说话的人和电话虫有相同的行为所以需要为二者提供一个抽象类电话虫是在为说话的人办事所以电话虫和说话人应该有关联关系。
根据上面的描述先把对应的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. 海上餐厅
在海贼世界中巴拉蒂是位于东海桑巴斯海域的一个海上餐厅。外形是条巨型的船船两头有鱼形状的船首整艘船能转换成战斗状态。巴拉蒂是东海最有名的餐厅有不少人特地为了品尝老板兼主厨哲普所做的美味料理而来到这里甚至连海军的重要角色都会来这里吃饭。 当出身东海的路飞路过巴拉蒂餐厅的时候由于损坏了餐厅的财物被强行扣留下来打工以此偿还他造成的损失替自己赎身。这天来了几个人吃饭路飞作为服务员接待他们点餐。意料之中他又把事儿搞砸了下面作为程序猿的我打算替路飞写一个点餐的小程序先来分析需求
允许顾客点多个菜点餐完毕后厨师才开始制作点餐过程中需要及时提醒顾客这个菜现在是不是可以制作可能原材料用完了需要有点餐记录结账的时候用顾客可以取消已下单但是还没有制作的菜 如果想要实现上述的需求需要在程序中准备如下几个对象
替顾客下单的服务员路飞给顾客炒菜的厨师哲普由路飞写好的顾客点餐列表
我们可以将顾客的点餐列表看作是一个待执行的命令的列表这样就可以总结出三者之间的关系了厨师哲普是这些命令的接收者和执行者路飞是这些命令的调用者。如果没有这张点餐列表路飞需要非常频繁地穿梭在餐厅与厨房之间而且哪个顾客点了什么菜也容易弄混从某种程度上讲这个点餐列表就相当于一个任务队列。
上面的这种解决问题的思路用到的就是设计模式中的命令模式。命令模式就是将请求转换为一个包含与请求相关的所有信息的独立对象通过这个转换能够让使用者根据不同的请求将客户参数化、 延迟请求执行或将请求放入队列中或记录请求日志 且能实现可撤销操作。
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()){listAbstractCommand* 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:// 存储顾客的下单信息mapint, listAbstractCommand* 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类图再写程序。 命令模式最大的特点就是松耦合设计它有以下几个优势
使用这种模式可以很容易地设计出一个命令队列对应路飞类中的点餐列表可以很容易的将命令记录到日志中对应例子中的账单信息允许接收请求的一方决定是否要否决请求对应例子中的鱼香肉丝可以很容易的实现对请求的撤销和重做对应例子中的撤单函数
文章作者: 苏丙榅
文章链接: 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.hNode* 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.hNode* 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()
{vectorstring 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:mapstring, 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 解构历史
历史的进程有一定的脉络作为程序猿的我们可以把历史拆分为这么几个部分
历史中的亲历者个人或群体历史中发生的事情的来龙去脉记录历史的人
这三部分数据也是备忘录模式中的三要素事件的主体、事件的内容、事件的记录者。在事件主体上发生的事情就是事件的内容事件的内容通过事件记录者进行备份。很显然在备忘录模式中将事件的主体和事件的内容进行解耦合更有利于程序的扩展和维护。根据上面的描述我们就可以把这三者之间的对应关系通过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:mapint, History* m_history;
};在这个类中将所有的历史信息通过map 容器存储了起来
通过addHistory 函数 添加历史信息并保存通过getHistory 函数 从备份信息中得到想要的历史信息
2.3 罪证
纵观中国历史古人告诉我们对付倭国的这帮畜生只有一个办法就是使劲揍打的越疼它们越顺从。最后来看一下倭国被收拾之后承认的对我国犯下的累累罪行
C
int main()
{vectorstring 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 发布者
摩根斯的新闻社是一个报纸发行机构内容大多是与当前的世界格局和各地发生的事件有关其他人也可以发报纸为了避免竞争可以更换一下题材比如海贼们的八卦新闻不管是哪一类都需要满足以下的需求
添加订阅者将所有的订阅者存储起来删除订阅者将其从订阅者列表中删除将消息发送给订阅者发通知
根据上述信息我们就可以先创建出一个发布者的抽象类
头文件 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:// 订阅者列表listObserver* 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.hclass 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;
};在山治类中有两个私有成员变量:
m_clock通过这整形的时钟变量来描述一天中当前这个时刻的时间点m_state通过这个状态指针来保存当前描述山治状态的对象
关于山治类的成员函数有以下这么几个
working() 工作函数在不同的时间状态下工作的内容也不同setClock() 设置当前的时间getClock() 得到当前的时间setState() 设置山治当前的状态
2.3 工作日志
我们可以修改山治类内部的时钟的值来模拟时间的流逝这样山治的状态就会不停地发生变化了
C
int main()
{Sanji* sanji new Sanji;// 时间点vectorint 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年的天才科学家贝加庞克设计机器人的思路类似在编程的时候也有一种类似的设计模式叫做模板方法模式。模板方法模式就是在基类中定义一个算法的框架允许子类在不修改结构的情况下重写算法的特定步骤。说的再直白一些就是先定义一个基类在基类中把与需求相关的所有操作函数全部作为虚函数定义出来然后在这个基类的各个子类中重写父类的虚函数这样子类基于父类的架构使自己有了和其他兄弟类不一样的行为。模板方法这种设计模式是对多态的典型应用。
模板方法这种模式在显示生活中的应用场景也有很多比如 盖房子地基、建造架构相同但是水电安装却可以不一样 造车、造船使用相同的车、船架构可以造出很多不同型号的车、船 考试卷试题相同但是每个人书写的答案却不尽相同。
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. 遭遇香波地
在海贼世界中香波地群岛位于伟大航路中间的红土大陆前方。岛屿是由多颗大树构成地面就是树根会从地面冒出气泡。在这里路飞的好朋友人鱼凯米被人贩子拐卖并卖给了天龙人路飞为了救凯米狠狠地揍了天龙人。最后草帽团被巴索罗缪·大熊的果实能力拍到了世界各地草帽团就地解散。 对于当时的草帽团来说一共有九个人面对这个打不过的敌人大熊九个人都做了最后的反抗和挣扎在这个过程中这些人的状态和行为或许相同相互不同归纳总结一下就是无助、恐惧、愤怒。 如果我们想要通过程序复刻上面动态图片中的这个场景是有很多种处理方式的最简单的一种就是定义一个人的基类然后让草帽团的各成员作为这个类的子类在各个子类中来具体描述他们面对大熊的攻击时的反应和状态。
索隆很愤怒决定要砍了大熊山治很愤怒决定要踢死大熊乌索普很恐惧在心里画圈圈诅咒大熊路飞很愤怒想锤死大熊乔巴很愤怒想拍死大熊布鲁克很愤怒用已死的身体阻挡大熊弗兰奇很愤怒思考怎么弄死大熊娜美很恐惧请求伙伴的帮助罗宾很恐惧请求伙伴的帮助
可以看到这么写有一个弊端就是比较啰嗦其实草帽团面对大熊的突如其来的攻击就两个状态愤怒和恐惧在这两种状态下的反应就是战斗和求助。所以我们可以把上面的这个场景重构一下
如果草帽团的某些成员在面对大熊攻击时的状态反应是一样的那么在这些子类中就会出现很多相同的冗余代码。有一种更好的处理思路就是将状态和人分开其中草帽团的各个成员我们可以看做是对象草帽团成员的反应和状态我们可以将其看做是算法这种将算法与其所作用的对象隔离开来的设计模式就叫做访问者模式其实就是通过被分离出的算法来访问对应的对象。
关于访问者模式在日常生活中对应的场景也有很多比如 旅游去安徽可以爬黄山去山东可以爬泰山去陕西可以爬华山 不同的地点人爬的山是不一样的 卖保险如果是老百姓推销医疗保险如果是银行推销失窃保险如果是商铺推销火灾和水灾保险。 不同的受众保险推销员推销的产品是不一样的 小鬼子奇葩的盖章文化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);}
};在上面的代码中用到了一种双分派技术
在调用成员类的accept() 函数的时候将具体地行为状态通过参数传递给了男性成员或者女性成员。在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:listAbstractMember* m_members;vectorAbstractAction* m_actions;
};在这个类中使用了两个容器:
list 容器用于存储草帽团成员vector 容器用于存储草帽团成员的两种行为状态。
通过这个类的display()函数我们就可以了解到草帽团成员在被大熊拍飞之前的行为状态了。
2.3 解散再见
草帽团成员已集合完毕大熊可以下手了
C
int main()
{srand(time(NULL));vectorstring 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);}
};在上面的代码中用到了一种双分派技术
在调用成员类的accept() 函数的时候将具体地行为状态通过参数传递给了男性成员或者女性成员。在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:listAbstractMember* m_members;vectorAbstractAction* m_actions;
};在这个类中使用了两个容器:
list 容器用于存储草帽团成员vector 容器用于存储草帽团成员的两种行为状态。
通过这个类的display()函数我们就可以了解到草帽团成员在被大熊拍飞之前的行为状态了。
2.3 解散再见
草帽团成员已集合完毕大熊可以下手了
C
int main()
{srand(time(NULL));vectorstring 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 许可协议。转载请注明来自 爱编程的大丙
文章转载自: http://www.morning.xdnhw.cn.gov.cn.xdnhw.cn http://www.morning.ckdgj.cn.gov.cn.ckdgj.cn http://www.morning.plzgt.cn.gov.cn.plzgt.cn http://www.morning.ywpwg.cn.gov.cn.ywpwg.cn http://www.morning.nwynx.cn.gov.cn.nwynx.cn http://www.morning.xhwty.cn.gov.cn.xhwty.cn http://www.morning.qrsm.cn.gov.cn.qrsm.cn http://www.morning.cjcry.cn.gov.cn.cjcry.cn http://www.morning.ljpqy.cn.gov.cn.ljpqy.cn http://www.morning.mnclk.cn.gov.cn.mnclk.cn http://www.morning.lpmjr.cn.gov.cn.lpmjr.cn http://www.morning.skbbt.cn.gov.cn.skbbt.cn http://www.morning.gmwqd.cn.gov.cn.gmwqd.cn http://www.morning.gwjsm.cn.gov.cn.gwjsm.cn http://www.morning.ryjl.cn.gov.cn.ryjl.cn http://www.morning.sfwcx.cn.gov.cn.sfwcx.cn http://www.morning.wxlzr.cn.gov.cn.wxlzr.cn http://www.morning.kwrzg.cn.gov.cn.kwrzg.cn http://www.morning.htbbp.cn.gov.cn.htbbp.cn http://www.morning.fjscr.cn.gov.cn.fjscr.cn http://www.morning.ksjnl.cn.gov.cn.ksjnl.cn http://www.morning.mdrnn.cn.gov.cn.mdrnn.cn http://www.morning.dysgr.cn.gov.cn.dysgr.cn http://www.morning.nlrxh.cn.gov.cn.nlrxh.cn http://www.morning.rwlsr.cn.gov.cn.rwlsr.cn http://www.morning.brfxt.cn.gov.cn.brfxt.cn http://www.morning.ppgdp.cn.gov.cn.ppgdp.cn http://www.morning.gdgylp.com.gov.cn.gdgylp.com http://www.morning.qjzgj.cn.gov.cn.qjzgj.cn http://www.morning.ktblf.cn.gov.cn.ktblf.cn http://www.morning.ccffs.cn.gov.cn.ccffs.cn http://www.morning.hmqjj.cn.gov.cn.hmqjj.cn http://www.morning.thrtt.cn.gov.cn.thrtt.cn http://www.morning.rkdzm.cn.gov.cn.rkdzm.cn http://www.morning.27asw.cn.gov.cn.27asw.cn http://www.morning.kyzxh.cn.gov.cn.kyzxh.cn http://www.morning.jqllx.cn.gov.cn.jqllx.cn http://www.morning.rsszk.cn.gov.cn.rsszk.cn http://www.morning.sfrw.cn.gov.cn.sfrw.cn http://www.morning.tkxr.cn.gov.cn.tkxr.cn http://www.morning.kxqfz.cn.gov.cn.kxqfz.cn http://www.morning.khntd.cn.gov.cn.khntd.cn http://www.morning.zxwqt.cn.gov.cn.zxwqt.cn http://www.morning.fwnyz.cn.gov.cn.fwnyz.cn http://www.morning.dqkcn.cn.gov.cn.dqkcn.cn http://www.morning.jqsyp.cn.gov.cn.jqsyp.cn http://www.morning.bzgpj.cn.gov.cn.bzgpj.cn http://www.morning.gbnsq.cn.gov.cn.gbnsq.cn http://www.morning.wmfr.cn.gov.cn.wmfr.cn http://www.morning.kjawz.cn.gov.cn.kjawz.cn http://www.morning.srhqm.cn.gov.cn.srhqm.cn http://www.morning.mzwqt.cn.gov.cn.mzwqt.cn http://www.morning.pwqyd.cn.gov.cn.pwqyd.cn http://www.morning.mlffg.cn.gov.cn.mlffg.cn http://www.morning.hwcln.cn.gov.cn.hwcln.cn http://www.morning.pbsqr.cn.gov.cn.pbsqr.cn http://www.morning.hqwcd.cn.gov.cn.hqwcd.cn http://www.morning.rfycj.cn.gov.cn.rfycj.cn http://www.morning.ngdkn.cn.gov.cn.ngdkn.cn http://www.morning.xcjwm.cn.gov.cn.xcjwm.cn http://www.morning.hqwxm.cn.gov.cn.hqwxm.cn http://www.morning.ndxss.cn.gov.cn.ndxss.cn http://www.morning.bpmfq.cn.gov.cn.bpmfq.cn http://www.morning.elmtw.cn.gov.cn.elmtw.cn http://www.morning.zfrs.cn.gov.cn.zfrs.cn http://www.morning.sqlh.cn.gov.cn.sqlh.cn http://www.morning.zlhzd.cn.gov.cn.zlhzd.cn http://www.morning.dzqr.cn.gov.cn.dzqr.cn http://www.morning.mxptg.cn.gov.cn.mxptg.cn http://www.morning.stsnf.cn.gov.cn.stsnf.cn http://www.morning.mdnnz.cn.gov.cn.mdnnz.cn http://www.morning.znpyw.cn.gov.cn.znpyw.cn http://www.morning.wdwfm.cn.gov.cn.wdwfm.cn http://www.morning.bangaw.cn.gov.cn.bangaw.cn http://www.morning.dnycx.cn.gov.cn.dnycx.cn http://www.morning.dwgcx.cn.gov.cn.dwgcx.cn http://www.morning.itvsee.com.gov.cn.itvsee.com http://www.morning.wrysm.cn.gov.cn.wrysm.cn http://www.morning.jgnst.cn.gov.cn.jgnst.cn http://www.morning.stwxr.cn.gov.cn.stwxr.cn