ing,整理常见的设计模式
策略模式
概念
在软件开发中也常常遇到类似的情况,实现某一个功能有多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…或者case等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。如果我们将这些策略包含在客户端,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。
策略模式把对象本身和运算规则区分开来,其功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性的思想。
例子:策略模式代替if-else
需求
根据用户vip等级来返回不同的价格,vip等级是不固定的,随时可能要增加,价格也不是固定的。
if-else写法
1 | /** |
使用策略模式
定义策略接口
1 | /** |
不同算法实现类
1 | public class VipOneStrategy implements Strategy { |
定义上下文,上下文持有策略接口的引用
1 | /** |
单例模式
单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。
C++11之前
在C++11之前,实现一个通用的泛型单例模式时,会遇到一个问题:这个泛型单例要能够创建所有的类型对象,但是这些类型的构造函数的形参可能不同,参数个数和参数类型都可能不同,导致我们不容易做一个所有类型都通用的单例。
假设我们的函数形参的个数上限可以确定,比如是3,我们可以用重载来解决。
1 | template <typename T> |
C++11之后
但是一旦参数个数超过3个,上面的类就没法解决了,而且有大量的的重复定义,非常得冗余,如果用上可变参数模版,就可以解决这一系列的问题了。
1 | template <typename T> |
观察者模式
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所以依赖于它的对象都得到通知并被自动更新。这让我联想到了zookeeper里的监控模式,一旦监控的对象发生改变,自己就会主动去拉去新的状态,有一些类似。公众号订阅采用这种方式也不错,不用考虑何时更新文章,在收到发生变化的信号时,主动去拉去就可以了。
具体的观察者模式的概念:https://www.runoob.com/design-pattern/observer-pattern.html
概念
看了链接里的概念之后,可以了解到,观察着模式,需要几个最基本的类:
- Subject(观察的对象)
- Obserer(顶层的观察者,观察着的基类)
- Event(不同的实践,派生自观察者)
C++11之前
1 | class Observer { |
C++11之后
这段代码还是比较通俗易懂的,但这种实现不够通用,只有特定的观察者才有效:
- 必须是Observer的派生类才行
- 并且这个观察者还不能带参数
这两点限制让这段代码实现的观察者模式不够灵活,接下来通过C++11来做一些改进,解决上述提到的问题。
1 | class Observer_new { |
这段代码解决了上述的问题,同时结构上有了很大的改变,我们不在自定义一个subject了,因为坚听的参数可以由我们自己确定,同时也不需要定义多个监听类了,因为除了函数可以直接由我们自定义,变化很大,但是非常简洁。
访问者模式
概念
访问者模式表示一个作用于某对象结构中的各元素的操作,可用于在不改变各元素的类的前提下定义作用于这些元素的新操作。通过这种方式,元素的执行算法可以随着访问者改变而改变。
C++11前
1 | struct ConcreteElement1; |
C++11后
在访问者模式中,被访问者应该是一个稳定的继承体系,如果这个体系经常变化,就会导致经常修改Visitor基类,因为在Visitor基类中定义了需要访问的对象类型,每增加一种被访问类型就要增加一个对应的纯虚函数,这是不太稳定的。
根据面向接口编程原则,我们应该依赖于接口而不应该依赖于实现,因为接口是稳定的,不会变化的。而访问者模式的接口不太稳定,存在很大的隐患。要解决这个问题,最根本的方法是定义一个稳定的Visitor接口层,即不会因为增加新的被访问者而修改接口层,通过C++11的改进,就可以实现。
1 | template <typename... Types> |
具体一些重要的部分,我写在注释里了,像上面这段代码这样写,改进之后的访问者模式把Visitor接口层的变化转移到了被访问者基类对象中了,这比最初的访问者模式要更容易维护。当然了,具体的访问者里的实际的方法还是需要添加的,被访问者的具体类型也需要,但是访问者基类就不需要不断的添加对应的虚函数了。
命令模式
命令模式的作用是将请求封装为一个对象,将请求的发起者和执行者解耦,支持对请求排队、撤销和重做。由于将请求都封装成一个个命令对象了,使得我们可以集中处理或延迟处理这些命令请求,而且不同的客户端可以共享这些命令,还可以控制优先级,将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
命令模式的耦合性非常优秀,但是随着请求的增多,命令类也会增多,尤其是在GUI中,越来越多的命令类会导致类爆炸。关于类膨胀的问题,GOF很早就意识到了,他们提出了一个解决方案:对于简单的不能取消和不需要参数的命令,可以用一个命令类模版来参数化该命令的接收者,用接收者类型来参数化命令类并维护一个接收者对象和一个动作之间的绑定,这一动作是用指向同一成员函数的指针存储的。
原始代码
这一部分代码直接从GOF书中获得,要是需要更详细的解释,可以直接去看书,我稍微改了一点让他可以运行。
1 | class Command { |
可以看到,它只能是简单的命令类,不能对复杂的甚至所有命令类泛化。
要解决命令模式类爆炸问题,关键是如何定义通用泛化的命令类,需要让命令类能够接受所有的成员函数指针或者函数对象,需要一个函数包装器。
我直接看了作者的代码,对于C++初学者的我来说,非常不友好,我做了很多注释。不过还需要提前了解一下member function pointer。
成员函数指针
1 | struct A { |
改进后的代码
1 | template<typename Ret = void> |
对象池模式
对象池对于创建开销比较大的对象来说很有意义,为了避免重复创建开销比较大的对象,可以通过对象池来优化,我觉得编写思路可以类比一下线程池,或者是数据库的连接池。避免重复创建对象,提高程序性能。
经典款代码
这里直接给出书上的代码,稍微改了一下,不然编译不通过,看一下就可以了,经典款。
1 | const int MaxObjectNum = 10; |
这个对象池的实现很经典:初始创建一定量的对象,用的时候从池子中取出,用完之后在放回去。
一些经典的对象池模式往往有不足之处:
- 对象用完之后需要手动回收,存在忘记回收的风险(其实用了智能指针就可以了,我第一版就用了)
- 不支持参数不同的构造函数(这里其实已经用可变参数解决了)
改进后的代码
其实差别不大,我只不过把作者写的贴了一下,可以看到差距其实不大的。
1 | using namespace std; |
参考资料
- 菜鸟
- <<深入应用c++11>>
- <<Design Patterns, Elements of Reusable Object Oriented Software (Gang of Four)>>
本文链接: http://woaixiaoyuyu.github.io/2022/04/18/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%94%B6%E9%9B%86/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!