COSMIC的后端学习之路——1.4 + 1.5 设计模式
1.4 + 1.5 设计模式
- 知识树
- 1、怎么学习设计模式 / 如何判断是采用的是什么设计模式 / 怎么分析要使用什么设计模式(2)
- 2、4大主要的设计原则
- 3、8大设计原则
- (1)依赖倒置
- (2)开放封闭
- (3)面向接口
- (4)封装变化点
- (5)单一职责
- (6)里氏替换
- (7)接口隔离
- (8)组合优于继承
- 4、设计模式及代码实现
- (1)模板方法(C++中使用频率最高的)
- (2)观察者模式(分布式系统中使用最为频繁,事件驱动,actor模型(skynet)中使用频繁)
- (3)策略模式(用来消除if else代码块)
- (4)单例模式:项目中使用最频繁的
- (5)工厂方法
- (6)抽象工厂
- (7)责任链模式
- (8)装饰器模式
- 5、感谢大家的观看,我是COSMIC
本博客总结自零声教育的课程
本博客将和大家一起学习一些常用的设计模式,告诉大家如何判断是采用的是什么设计模式,怎么分析要使用什么设计模式,向大家介绍8大设计原则和4大主要的设计原则,并结合代码和大家一起学习8种常用的设计模式
下述内容相关的代码均在我的github中,代码中每条语句都有详细的注释,希望大家搭配代码来阅读以下内容,以下是我的github代码链接,恳请大家批评指正!
GitHub代码
代码在1.4_1.5_designpattern文件夹中
知识树
1、怎么学习设计模式 / 如何判断是采用的是什么设计模式 / 怎么分析要使用什么设计模式(2)
分为两步:
1、找稳定点和变化点;封装稳定点,隔离出变化点(设计模式的本质)
2、对于简单的设计模式(比如单例模式和策略模式),直接使用;对于复杂的策略模式,要【★★★先满足8大设计原则,再慢慢迭代出设计模式】
设计原则之间是 互相交叉 的关系:
2、4大主要的设计原则
★①开闭原则(写代码时优先考虑):对扩展开放、对修改关闭
★②单一职责:一个类只有一个变化方向,意思是告诉我们【不要修改类而应该扩展类】
★③里氏替换(说的是【继承】过程中的问题):扩展有 组合 和 继承 两种方式,里氏替换是说【继承的时候一定要继承父类的职责(即实现父类的功能)】
★④接口隔离:使用protected等限定词让用户不要选择他们不用的接口
因此,能够看出★上述四种设计原则的关系:开闭原则是总体,单一职责、里氏替换、接口隔离都是在佐证开闭原则
★满足这四个设计原则,很快就能迭代出设计模式
PS:刚开始可能不会一下子找到设计模式,所以要先满足设计原则,随着后面业务需求的变更,在迭代的过程中慢慢会将设计模式呈现出来
举例:整洁的房间,好动的猫,如何保持房间的整洁?
措施:把猫关在笼子里面,就是把变化点隔离起来;解耦合,使得若需求发生变化时,需求之间相互不影响(写框架来实现具体功能的时候希望需求是稳定的,不会因为某个需求的变化造成代码重写或代码重构);解耦合不是把耦合消灭掉,而是把耦合限制在一定范围内,让变化点在可控范围内进行活动
例如:锁粒度 或 耦合度,只有降低锁粒度 或 降低耦合度,不是消除锁粒度或消除耦合度
★编程需要的两种思维:
1、抽象思维(将具体需求抽象成一些固定的编程的套路,即设计模式)
2、分治思维(将大的模块拆分成小模块,从局部出发,慢慢到全局)
3、8大设计原则
(1)依赖倒置
①高层模块不应该依赖低层模块,两者都应该【依赖抽象】
②抽象不应该依赖具体实现,具体实现应该依赖于抽象
高层模块可以想象成应用层,底层模块可以想象成一个固定的框架
在高层和底层中间抽象出一个标准,自动驾驶系统研发公司和不同汽车厂商是面向整个标准,而不是自动驾驶系统研发公司直接面向不同汽车厂商的需求;这样一来,不同汽车厂商可以根据各自的需求来调用自动驾驶标准的接口即可
这就是解耦合(解自动驾驶系统研发公司 和 不同汽车厂商的耦合),不是消灭耦合,而是将耦合降低在一个自动驾驶标准上面
自动驾驶系统公司是高层,汽车生产厂商为低层,它们不应该互相依赖,一方变动另一方也会跟着变动,而应该【抽象一个自动驾驶行业标准,高层和低层都依赖它】,这样一来就解耦了两方的变动,自动驾驶系统、汽车生产厂商都是具体实现,它们应该都【依赖自动驾驶行业标准(抽象)】
(2)开放封闭
C++中有哪些扩展方式:
1、继承
★继承有两种扩展方式:组合和继承
①组合:组合基类的指针
②继承:采用虚函数复写的方式进行扩展
①组合基类的指针:
a. 上方Subject类继承了Base类:在内存模型中:Base类中的成员在Subject类的前面,Subject类中的成员在Base类的后面
b. 下方Subject类组合了Base的对象(组合了类的对象):在内存模型中:Base类中的成员在Subject类的前面,Subject类中的成员在Base类的后面(与上面的继承一样)
★这说明1和2这两种情况下Subject和Base类的耦合度很高,因为在内存模型中是紧挨在一起的
c. ★★★Subject类组合了Base类的指针(★★★组合了类的指针):
★★★因为是基类的指针,所以可以有很多类可以继承自base,比如base1、base2、base3,未来哪一个子类传递给base指针,父类base就继承了哪一个子类(base1或base2或base3)的功能,体现了扩展性(即晚绑定)。因此应尽量【★★★组合基类的指针】而不是组合基类的对象
②★★★采用虚函数复写的方式进行扩展:
a. ★★★上方:当在类中创建了一个虚函数时,【内存布局】中有一个【虚函数的指针】,虚函数的指针【指向虚函数表】,虚函数表中会【存储着对应虚函数的函数指针】
b. 下方:★★★虚函数复写的方式进行扩展:当Subject继承了Base时,Subject中也有两个虚函数,这样子类Subject和父类Base中都有虚函数func2,★★★虚函数表中存储的是【★★★子类的】func2,即为子类可以进行虚函数复写,也体现了扩展
★★★类之间的关系:
1、陌生人只能访问托尼爸爸的a(public)
2、托尼只能访问托尼爸爸的a和b(因为托尼继承了托尼爸爸,能够访问基类的public和protected)
3、漂亮阿姨能访问托尼爸爸的a、b、c(因为托尼爸爸将漂亮阿姨设置为友元类,友元类可以访问该类的private、public和protected)
4、托尼爸爸不能访问漂亮阿姨的e(因为友元类是单向的,声明友元的类不能访问友元类的private)
5、托尼妈妈能够访问托尼爸爸的a、b、c(因为托尼爸爸将托尼妈妈设置为友元类)
6、托尼爸爸能够访问托尼妈妈的d(因为托尼妈妈将托尼爸爸设置为友元类)
(3)面向接口
面向接口: 不要考虑类中有哪些数据,要考虑类中有哪些接口(接口有哪些:①函数作为接口 ②纯虚函数的接口类)
不将变量类型声明为某个特定的具体类,而是声明为某个接口
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
减少系统中各部分的依赖关系,从而实现【“高内聚、松耦合”】的类型设计方案;
(4)封装变化点
封装变化点:将变化点在有限范围内变化不动,不要让变化点影响整个代码
将稳定点和变化点分离,扩展修改变化点,让稳定点和变化点实现层次分离
(5)单一职责
单一职责:职责 = 功能,一个类应该仅有一个引起它变化的原因(若有多个,则该类会变得不稳定)
(6)里氏替换
子类型必须能够替换掉它的父类型(主要是职责的替换);主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误;覆盖了父类方法却没有实现父类方法的职责,【子类一定要实现父类的职责】!!!
(7)接口隔离
接口隔离:(用好private和protected)
不应该强迫客户依赖于它们不用的方法
一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责
(8)组合优于继承
组合优于继承:【继承耦合度高,组合耦合度低】
4、设计模式及代码实现
(1)模板方法(C++中使用频率最高的)
定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中(★要有继承,才有子类)。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤(★★★子类复写父类的虚函数)。——《设计模式》GoF
项目背景:
某个品牌动物园,有一套固定的表演流程(对应定义中的算法骨架,是稳定点),但是其中有若干个表演子流程可创新替换(是变化点),以尝试迭代更新表演流程
1、找稳定点和变化点:
封装稳定点:
2、对于简单的设计模式(比如单例模式和策略模式),直接使用;对于复杂的策略模式,要先满足8大设计原则,再慢慢迭代出设计模式:
4大主要的设计原则:①开闭原则 ②单一职责 ③里氏替换 ④接口隔离
①接口隔离原则:不要让用户去选择他们不需要的接口
因为是固定的表演流程,所以要将show0、show1、show2函数封装成private,用户不能自己组合调用,这样【用户只有一个选项就是直接调用show函数】
②单一职责(类只有一个变化方向) 和 ③开闭原则(对扩展开放,对修改关闭)
扩展的思路:【原来的表演流程是一个基类】,我们去继承他,然后去【修改里面的子流程】即可
扩展是扩展变化点,变化点是show0、show1、show2、show3:
★扩展步骤:
a. 将show0、show1、show2、show3改为虚函数,
b. 不能用private,所以要用protected,因为:用户调用还是要调用父类的show,用protected的话,用户是调用不了的,但是子类是可以覆盖的(考虑接口隔离)
④里氏替换原则:替换的时候要实现父类的职责:因为Show0返回true后才能够实现playGame函数,因此子类替换Show0的时候,结果要返回true,这样流程才不会断掉,否则可能会出现bug
★使用的时候,创建一个基类,new一个版本信息即可:
要点:
1、模板方法是最常用的设计模式,子类可以复写父类子流程,使父类的骨架流程丰富
注意:【★★★看到不变的流程 且 子流程是可变的】,【首先想到使用模板方法】
2、反向控制流程的典型应用
★★★反向控制的思想:父类去定义他的骨架流程,子类去创新他的子流程(★★★总体流程由父类控制,子类去控制子流程);比如使用开源框架时,框架封装了固定流程,用户去调用开源框架时,我们只是去定制子流程
3、父类protected保护子类需要复写的子流程,这样子类的子流程只能父类来调用
★模板方法的本质:通过【固定算法骨架】来【约束子类行为】
模板方法的结构图:
父类AbstractClass的TemplateMethod模板方法就是去调用两个子流程Primitive1()和Primitive2()
★下面两个是子类的实现,复写了上面的AbstractClass的两个子流程Primitive1()和Primitive2()
(2)观察者模式(分布式系统中使用最为频繁,事件驱动,actor模型(skynet)中使用频繁)
定义:定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GoF
项目背景:
气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个不同的显示终端(A和B)
1、找稳定点和变化点:
变化点:数据中心处理数据可能会发生变化,显示终端的数量可能会发生变化,不同终端显示的气象信息的图标等可能会发生变化等
稳定点:气象站发布气象资料给数据中心,这个过程是不变的
2、对于简单的设计模式(比如单例模式和策略模式),直接使用;对于复杂的策略模式,要先满足8大设计原则,再慢慢迭代出设计模式:
要点:
观察者模式使得我们可以独立地改变目标与观察者,从而使二者之间的关系松耦合
观察者自己决定是否订阅通知,目标对象并不关注谁订阅了
观察者不要依赖通知顺序,目标对象也不知道通知顺序
常用在基于事件(监听读写事件)的ui框架中,也是 MVC的组成部分
常用在分布式系统中、actor框架中
★Notify(通知、唤醒)的调用时机是【数据变更的时候】:气象站发布数据给数据中心的时候会调用notify,notify实现以下几个功能:是数据中心调用Notify:
①计算数据
②将数据Notify到所有监听数据中心数据变化的终端上(数据变更的时候才会调用notify,notify才会广播给其他监听的对象)
★★★有【订阅、发布功能】的都会涉及到观察者模式
【不是主动请求,而是有监听变更,如果有变更的话推送】:
1、zk、etcd、kafka、redis
2、actor:用户态所抽象出来的轻量级的进程,进程之间通讯的时候需要监听,如果有数据变更的话会主动推送(不是主动请求数据)
3、分布式锁:
①公平锁:互斥锁,阻塞等待在这里,线程 / 进程会发生切换,等到锁释放的时候才会唤醒该线程 / 进程;公平锁使用的是发布订阅,只有变化的时候才会进行推送,锁被释放的时候会notify,所有其他线程 / 进程 / 协程会监听到锁被释放这一消息;
②非公平锁(不断地去请求锁是否被释放),例如自旋锁(空转cpu,不断地检测锁是否可用,不断地请求锁是否释放)
选择互斥锁还是自旋锁取决于操作临界资源时长:时长大于线程切换的时间则选用互斥锁(重io操作),时长小于线程切换的时间则选用自旋锁
锁粒度对应操作临界资源的时长
★观察者模式的本质:触发联动
结构图:
Observer相当于数据中心,ConcreteObserverA和ConcreteObserverB相当于显示终端的具体实现,ConcreteSubject相当于扩展给显示终端的接口
如果不是采用的发布订阅的模式,会不断请求他,把数据中心压垮
如果是发布订阅的模式,则【只有变化的时候才会进行推送】
(3)策略模式(用来消除if else代码块)
定义:定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用它的客户程序而变化。——《设计模式》GoF
背景:某商场节假日有固定促销活动(稳定点),为了加大促销力度,现提升国庆节促销活动规格(变化点)
1、找稳定点和变化点:
①稳定点:“节假日有固定促销活动”,将促销活动固定起来,让促销活动只和促销策略相关
②变化点:促销活动规格:促销活动里面的算法(每个节假日不同,促销活动不同)
★如果只有稳定点,则不需要设计模式:直接if else写下去即可,反正是不会变的
★如果只有变化点,则也不需要设计模式:用动态语言(脚本语言),例如游戏,一个星期就需要有一次版本的发布,很多功能都需要变更,所需用脚本语言更好
★既有稳定点又有变化点,则需要设计模式
★★★【接口当中】要【★★★将析构函数设置为虚函数】:如果不这样声明,则【子类的析构函数得不到正确的释放】
★抽象出节日策略的接口(父类中声明纯虚函数),该接口管控变化点变化的方向:
使得变化点都根据该接口来变化,例如春节:
要点:
1、策略模式提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换
2、策略模式【消除了条件判断语句,也就是在解耦合】
★策略模式的本质:分离算法,选择实现(将算法一个个抽象成接口,根据实际需求选择实现,来使用算法)
结构图:
Context用来选择不同的策略、选择不同的算法
ConcreteStrategyA和ConcreteStrategyB就是两个不同的算法
strategy是封装成的一个接口,相当于上面的ProStategy父类
如果全是稳定点或全是变化点,则不需要设计模式:
①全是稳定点:爱怎么写怎么写
②全是变化点:用脚本语言来写,经常更新迭代比较方便
(4)单例模式:项目中使用最频繁的
单例模式不需要分析稳定点和变化点
★单例模式的生命周期:单例模式与进程同生共死,只要声明了单例模式,单例模式就会跟着进程一直走,直到程序退出
定义:保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》GoF
单例模式的6个版本:
版本一:
1、声明一个静态的局部指针变量:
2、声明一个静态的函数,为了获取静态的局部变量,在静态的函数中new一个自己出来
3、★将构造函数和析构函数都保存在private中:
★构造函数放在private中是为了【保证全局仅有一个实例】,【保证在外面不能把他new出来】
★析构函数放在priavte中是为了【避免我们在外面直接delete他】,【如果在外面delete他的话别的地方使用就会报错,变成一个野指针了】
4、★★★将除了构造和析构函数以外的其他这些函数放到private中的意思是:【禁止了一个类构造时的这些行为】
C++11中也可以这样写:。。。 = delete,【不让默认产生】:
同样地也禁止在类构造时调用以下两个函数:拷贝构造函数 和 赋值函数:
这样写代码存在的问题:
1、多线程的问题:★多线程的问题没考虑,线程不安全:多线程到来时会多次new
2、没有释放堆区内存的问题:【★★★new的对象存放在堆区】,【★★★新建的静态对象_instance存放在全局区】;当单例随着进程退出的时候,之前new的Singleton()对象,他的析构函数没有被调到,因为他的析构函数被禁止了(在private中),因此有内存泄露的风险
版本二:
其中:
上述代码中,让【堆的内存释放】,采用进程退出的函数atexit,【析构前面堆区分配的内存】
下面代码中,delete是否可以释放上面new出的在堆区分配的内存?
答:可以,因为类中静态的函数(static void Destructor),与类是相关的,与类的对象是无关的,静态成员是可以调到private成员的
★★★类对象之间其实是友元的,友元可以调到private成员
★★★静态成员static void Destructor()和类是相关的,对象_instance又是我们的类,因此静态成员static void Destructor()和_instance之间是友元的关系,所以static void Destructor()可以调到类的private成员,即若析构函数在private中,则static void Destructor()还是可以调到析构函数
版本二解决了没有释放堆区内存的问题,但是还存在多线程问题
版本三:★★★利用【双检测 / 懒汉模式】解决多线程问题
★版本三:解决多线程的问题:我们希望只有一次new,避免对象被重复创建,即避免多次new
情况一:在3.1处加锁:
这样没有问题,但是操作太重了:因为,我们大部分调用GetInstance的时候是利用_instance这个指针,比如有10000次,其中只有第一次调用了new,其他9999次都只是获取_instance这个指针;上述使用的是互斥锁,互斥锁会切换线程,切换线程会降低我们获取的这个单例的性能
所以我们没必要在3.1处加锁,可以在3.2处加锁:
单例只会调用一次new的操作,new这里只会进行一次操作,每次都要加锁
若没有3.2下面这行的第二次if检测,即下面这样是否有问题?
这样还是会存在多线程问题,可能会多次new
原因:假设同时有两个线程走到了箭头处,只有一个线程会拿到锁,拿到锁后会new这个对象,然后释放锁,这时第二个线程又进来了,又new了一次,因此可能会出现多次new的情况
解决方案:★★★利用if语句进行双检测:
但是这样还是会存在cpu reorder问题:
★cpu reorder问题: new这句话可以拆解为三个步骤:
①分配内存
②调用构造函数
③返回指针
★多线程环境下,cpu有一个reorder操作,即本来的执行顺序是123,但是cpu的reorder操作后可能的执行顺序为132,就会出现先分配内存、再返回指针,但是没有调用构造函数,没有调用构造函数的话就会使得_instance没有被初始化;这样的话,如果有两个线程,第一个线程拿到锁后进来,但是_instance没有被初始化,第二个线程进不来(因为 在if(_instance == nullptr)的时候被挡在外面了),使得最终最终_instance没有被初始化,会发生【踩内存】的现象:在后面用gdb进行断点调试的时候,会发现类的对象中所有字段都有,但是字段中全部都是乱码(很大的值或看不懂的值):
综上,版本三中还是会存在cpu reorder的问题
以后cpu的reorder操作还会在linux内核中进行深入讲解
版本四:解决cpu reorder的问题
★解决cpu reorder问题:
在C++11中加入了【内存屏障】的接口,使用内存屏障会使得new操作按123的顺序执行,不会出现cpu reorder
★★★【添加内存屏障】的固定步骤123:
1、添加头文件 #include(原子操作)
2、用memory_order_relaxed来获取内存屏障,将_instance load为tmp:
3、用memory_order_release来释放内存屏障,再将tmp给_instance:
★为什么会出现cpu reorder,为什么cpu reorder会将123的顺序变为132?
因为采用13的执行速度比2快,cpu优化的时候是帮我们这么优化的(cpu reorder是一种优化的方式,会让程序运行更快)
版本五:C++ Effective的作者写的,用于解决 多线程问题 和 cpu reorder问题(利用C++11 的magic static特性,版本五没有任何问题)
(上面版本的析构函数都应该放在private中,该版本因为静态局部变量可以自动析构,所以该版本五的析构函数放在哪里无所谓)
没有使用静态类的全局变量,而是采用静态对象的方式:
★★★C++11的magic static特性:如果当变量在初始化的时候,并发同时进入声明语句,一个线程会进入初始化,并发线程将会阻塞等待初始化结束,因此没有多线程的问题:
★★★避免多线程问题的具体原因:静态变量只会被初始化一次,如果有并发线程访问,则只会有一个线程进来初始化静态对象,其他线程会阻塞等待
静态局部变量存放在静态全局区,和全局对象存储在一个区中
问题:构造静态的局部变量,是否可以调到析构函数?
答:可以调到析构函数,因为静态局部变量是系统自动帮我们释放内存的,会自动调用析构函数,只有堆上的内存才需要我们手动释放
综上,这样写的话:①没有cpu reorder的问题 ②多线程的问题也考虑到了
存在的问题:扩展性差,如果有多个单例的话比较麻烦,每个单例都需要这样声明:
版本六:★★★单例模板类
如果有多个单例的话需要继承Singleton
问题1:能否将父类的protected改为private?
答:不能,因为如果是private的话,子类继承父类后是无法调用父类的析构函数的
或者保留private,但是在上面添加友元(声明友元的话是可以调用private成员的):
问题2:子类中为什么要将父类声明为友元?
父类:
子类:
答:为了让父类能够访问子类的析构函数(子类的析构函数在private中)
T是子类,即DesignPattern,父类是Singleton;父类中声明了子类的对象,因此当然父类也需要调用子类的构造函数和析构函数(子类的构造函数在private中)
如果面试时要你手撕单例模式,要能够写出版本五或版本六(最好是版本六单例模板类)
(5)工厂方法
★看到【延迟】或【子类】想到【继承】
定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化延迟(继承)到子类。——《设计模式》GoF
在什么情况下要使用工厂方法:
★★★解决创建过程比较复杂,希望对外隐藏这些细节的场景:
1、比如连接池、线程池
创建过程比较复杂:连接池中构造函数比较复杂,有初始连接数、最大连接数、水平连接数等,需要比较复杂的构造函数(线程池也是有很复杂的创建流程);要面向接口编程,即我们不希望将这些东西让用户看到,只需要让用户通过接口获取对象即可
2、隐藏对象真实类型:迭代更新很多版本的时候:
为了隐藏v1 v2 v3,用工厂的方式将这些细节都屏蔽掉,不让用户看见v1v2v3,只是让用户看见实现的功能即可
3、对象创建会有很多参数来决定如何创建
4、创建对象有复杂的依赖关系:比如创建A类时需要依赖B类、C类、D类
项目背景:实现一个导出数据的接口,让客户选择数据的导出方式
1、找稳定点和变化点:
稳定点:导出
变化点:不同的导出方式
★★★抽象稳定点,让稳定点变得更加稳定,让变化点来扩展稳定点
2、八大设计原则:
①因为要面向接口编程,有不同的导出方式,所以将导出方式抽象成一个接口
主函数下面这样写存在的问题:new的时候可能还会有很多操作,所以最好用工厂方法屏蔽掉new操作,将new操作抽象一下(工厂方法主要是屏蔽创建过程中的一些细节)
解决:抽象出new操作:
(红线部分可能还有很多操作或参数,工厂模式为了屏蔽这些复杂的创建操作,因此把new抽象出来)
★★★怎么使用工厂方法:封装一个操作的流程,采用组合的方式(组合工厂基类指针)
★★★将稳定点(稳定的流程)往基类放(反向控制的思想)
★反向控制的思想:父类控制子类,子类只定制父类里面的子流程,相同的内容都往基类塞
(6)抽象工厂
定义:提供一个接口,让该接口负责创建一系列“【相关】或者【相互依赖】的对象",无需指定它们具体的类。——《设计模式》GoF
★看到依赖性,想到抽象工厂
工厂方法对比抽象工厂:
★工厂方法:只有导出这一个职责;工厂方法是隐藏复杂的创建过程的
★抽象工厂:解决工厂方法中有多个职责,他们之间的相互关系、可能相互依赖,抽象工厂是隐藏相关性和依赖性的
项目背景:实现一个拥有导出导入数据的接口,让客户选择数据的导出导入方式
举的这个例子,在上述工厂方法的基础上直接在工厂基类中扩展即可,这个例子只利用了抽象工厂来解决相关性,并没有解决依赖性,实际工作中利用抽象工厂解决依赖性较多
(7)责任链模式
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系(解两个对象之间的耦合关系,观察者模式也是解两个对象之间的耦合关系)。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。——《设计模式》GoF
项目背景:请假流程,1天内需要主程序批准,3天内需要项目经理批准,3天以上需要老板批准
★责任链的精髓:比如请2天假,先给主程序,主程序处理不了,再给项目经理,项目经理处理过后就不需要给老板了(老板不需要知道),若处理结束则责任链条可以【直接断掉】:
1、找稳定点和变化点
稳定点:处理流程是稳定的:主程序->项目经理->老板
变化点:请假天数,交由谁来处理(可能增加链条的长度)
代码1:
这样写代码的问题:
若后面要添加处理的人,即增加链条的长度,则处理流程(稳定点)这里需要进行修改,因此变化点(交由谁来处理)的变化使得稳定点(处理流程)变得不稳定了
1、抽象出【处理流程】这个稳定点:
2、递归调用GetNextHandler
对于复杂的依赖:
责任链模式举例:nginx处理http的请求的流程也是用的责任链模式(在openresty中查看这个过程的代码)(nginx将http协议的处理流程分为11个阶段):
①客户端要先通过http和服务端建立连接:先建立连接accept:客户端发送一个http请求到nginx,nginx要先accept:
clintfd,客户端的文件描述符;accept函数的第二个和第三个参数(addr和sz)是用来告诉我们客户端地址和大小的(知道ip地址可以做黑白名单等)
②把header(头)取出来
将header取出来可以做什么:登录验证:token是放到header中的,做登录的时候token验证
③再把body(内容)取出来,取出body后做具体的业务。。。
过程图:
上述流程可以很好地利用责任链模式:若在上述某个阶段中处理了,则直接退出,不需要向下继续进行了
★nginx原理:分成很多个阶段(在代码中为一个phase_handler),每个阶段有若干个步骤(圆圈表示,在代码中为一个handle(模块 / 功能)),但是每个步骤都有指针指向下一个阶段的第一个步骤,目的是为了:★如果该步骤执行过后不执行下面的步骤,则可以直接跳到下一个阶段(相当于责任链模式的变种):
nginx的责任链处理流程:
每一个阶段都有一个check(check是处理该阶段的自己的流程),如果步骤中有一个rc == NGX_OK(即该阶段的责任链被打断了,需跳到下一个阶段),则从该步骤返回,该阶段也结束,跳到下一个阶段继续执行
★责任链的稳定点 是 责任链的处理流程:
责任链模式与策略模式的区别和联系:
责任链模式:事件处理是由多个流程构成的(顺序执行,某个流程处理完则直接返回)
策略模式:有多个方法可以实现这个职责,我们选择一个来实现
要点:
1、解耦请求方和处理方,请求方不知道请求是如何被处理,处理方的组成是由相互独立的子处理构成,子处理流程通过链表的方式连接,子处理请求可以按任意顺序组合
2、责任链请求强调请求最终由一个子处理流程处理,然后直接退出
3、责任链扩展就是功能链,功能链强调的是,一个请求依次经由功能链中的子处理流程处理:
功能链:和责任链差不多,但是每一个节点都需要处理 且 必须一直顺序执行到最后
4、将职责以及职责顺序运行进行抽象,那么职责变化可以任意扩展,同时职责顺序也可以任意扩展
(8)装饰器模式
定义:动态地给一个对象增加一些额外的职责。就增加功能而言,装饰器模式比生产子类(工厂方法)更为灵活。·——《设计模式》GoF
★装饰器模式类似于责任链中的功能链,但是与功能链的区别是装饰器模式是不需要关注顺序的(是没有顺序的)
项目背景:普通员立有销售奖金,累计奖金,部门经理除此之外还有团队奖金,后面可能会添加环比增长奖金,同时针对不同的职位产生不同的奖金组合(装饰器模式不需要关注顺序,即先计算哪个奖金不重要)
1、找变化点和稳定点
变化点:不同职责的人(不同职责的人可能会增加),奖金种类(奖金种类可能会变)
稳定点:扩展奖金的规则是稳定的:比如普通员工外层有销售奖金、累计奖金,部门经理外层除了这些还套有团队奖金,等等,一层一层往外扩展:
★计算奖金的时候从外圈向内圈递归计算奖金
★自己组合自己:CalcBonus里面有CalcBonus * cc
★装饰器模式和责任链模式的对比:
装饰器模式不注重顺序,注重的是最终呈现出来的结果(例如装饰一个房间,先装饰什么不重要,重要的是最终的布局)
责任链模式是链条按顺序执行,是有先后顺序的
5、感谢大家的观看,我是COSMIC
COSMIC的后端学习之路——1.4 + 1.5 设计模式相关推荐
- COSMIC的后端学习之路——1.3 海量数据去重的Hash与BloomFilter(布隆过滤器),bitmap(位图)
1.3 海量数据去重的Hash与BloomFilter,bitmap 知识树 1.海量数据查找的应用场景 2.平衡二叉树 3.哪些算法涉及二分查找 4.散列表 (1)hash冲突 (2)负载因子 (3 ...
- COSMIC的后端学习之路——1.1 随处可见的红黑树
1.1 随处可见的红黑树 1.知识树 2.红黑树的性质(3) 3.红黑树的使用(2) 4.红黑树用在哪里(举例) 5.判断是否为叶子节点的方法(2) 6.红黑树的旋转 (1)左旋 (2)右旋 7.操作 ...
- COSMIC的后端学习之路——2.1 C++11新特性(1)
2.1 C++11新特性(1) 知识树 1.智能指针 (1)std::shared_ptr:共享的智能指针 ①初始化 ②获取原始指针 ③指定删除器(自定义删除对象) ④一些错误用法 (2)std::u ...
- Java Web和Java后端学习之路
摘要: 每个阶段要学习的内容甚至是一些书籍.针对Java Web和Java后端开发 java学习这一部分其实也算是今天的重点,这一部分用来回答很多群里的朋友所问过的问题,那就是我你是如何学习Java的 ...
- [Java学习之路篇] 设计原则与设计模式
编程可谓博大精深,按照不同的思路逻辑.框架规范编写出来的程序运行的效率都大相径庭.本篇并不只针对Java程序,应适用于所有编写的程序,是编程世界中的一套方法论,俗称编程套路.它们会出现在目前很多大公司 ...
- 【实验室培训】大学生的Java后端开发学习之路(从App开发讲起)
文章目录 前言 一.浅谈如何开发一款App 1.一个好的idea 2.根据这个idea完善好具体的需求 3.分工合作 4.设计阶段 ①UI设计 ②接口设计 ③架构设计 5.开发阶段 6.测试阶段 7. ...
- 我的mongo学习之路
mongo学习之路 mongodb的安装,在这里就不做介绍了,不管是windows还是mac,网上都有教程,可以自行学习一下~~~ 一.启动 mongod 复制代码 二.链接 mongo 复制代码 三 ...
- pyqt5从子目录加载qrc文件_【JVM系统学习之路】一篇看懂类加载
JVM系统学习之路系列演示代码地址:https://github.com/mtcarpenter/JavaTutorial 嗨喽,小伙伴大家好,我是小春哥,今天是打卡 [JVM系统学习之路] 的第二篇 ...
- 软件测试的学习之路-----基本的 HTML
文章目录 一:常见DOS命令 二:Web基本介绍 三:HTML基本介绍 四:HTML网页骨架 五:Hbuilder工具使用 六:HTML语法和标签基本介绍 七:图片标签 八:链接标签 九:路径 十:相 ...
- 软件测试的学习之路-----计算机基础 (详情展示)
文章目录 一:计算机基本介绍 二:硬件系统 三:软件系统 四:二进制的基本介绍 五:常见的数字进制 六:进制之间的转换 七:编码 八:数据的计量单位 九:编程语言 十:基本的DOS命令 十一:欢迎查看 ...
最新文章
- 标签修正:CVR预估时间延迟问题
- 深入浅出SQL(2)——select、update…
- mysql sql执行过程_MySQL探秘(二):SQL语句执行过程详解
- 美团推出外卖版拼多多;iOS 14 Beta 3暂禁用3D Touch功能;Rust 1.45 发布| 极客头条
- NYOJ-97兄弟郊游问题
- iOS底层:PAGEZERO的作用
- 代码审计工具学习之Seay(安装以及初步认识)
- SQL 笛卡尔积 学习与理解
- 每周推荐短视频:道哥表达了对自动驾驶技术的感恩之情
- 上海交大计算机系分数线2019,2019年上海部分高校各专业录取分数线汇总|附2019上海交通大学上海生源情况!...
- 纯前端canvas手绘海报
- 计算机登录账户删除著名恢复,win10系统删除计算机无用账户的恢复步骤
- 商务办公软件应用与实践【6】
- 一种改进的教与学优化算法
- weixin-java-pay实现公众号微信支付与退款
- “盘古”走向产业山峦,打开了一串AI落地的新脑洞
- Web前端:2022年十大React表库
- 华为手机开发人员选项哪里去了
- C# CSV文件读写
- 什么是Tower,Rack, blade服务器?