设计模式总结
参考 "Mastering Python Design Patterns",这本书的优点就是讲的通俗易懂,缺点就是模式不全(一共16种)和代码实例不够好(代码方面有些用了第三方库).
用 Python 实现了直译器模式以外的模式, 用 Racket 实现了原型模式和直译器模式以外的模式.
之所以没有实现直译器模式是因为直译器是一个大话题,不能三言两语讲清楚(主要是书上的例子跟 EOPL3 的讲解对比起来有点别扭),
至于 Racket 原型模式是因为我没有办法.
其中练习的时候参考了 二十三种设计模式及其python实现 这篇博客.之后会把剩下的6种补上(不算直译器模式,除去书中的 MVC 模式).
学后感
学习设计模式是我上一年的计划中的一个项目,现在终于抽空学得差不多了,所以第一感觉就是一身轻松.接下来就是我对设计模式的感受,
- 感觉实现设计模式可以做为一门新语言的上手练习,当然不支持
OOP的语言就算了.书上的例子是用Python写的,其实改用新学的语言写也是挺有乐趣的. 设计模式对
Lisp这种语言没什么必要,大部份情况下都可以用macro这种用于生成代码的杀手级特性来消除重复代码.而且
Lisp里面的结构体相对于其它语言的来说也是很强大,基本可以用它来替代类这种数据结构(根据Racket的类的打印来看,其实就是一种复杂一点的结构体).所以能够理解为什么
Racket用作用域来控制类的成员访问.总的来说,在Racket里面使用设计模式反而更复杂了,还没有macro更灵活.开始清楚
Django里面的一些代码设计的意图,不过这也反映了一个问题: "想要读懂那些规范代码的设计意图就需要学习设计模式".设计模式成为了业界的一种规范,我也不知道是好事还是坏事,好的方面是统一了项目的组织,懂设计模式的人交流起来更加轻松,
坏的方面就是加大了开发人员的学习成本.总体而言利大于弊,不过我内心是嫌弃设计模式的,因为我觉得设计模式就是
GOF4人帮当时从Java抽出来的东西,所以虽然是抽象层的东西,但用在别的语言身上局限性还是太大.之所以推荐学是因为它已经成为一种业界规范了,我想你应该不想与群体脱节吧.
从内容来看,它也的确是总结了一些写代码的常用套路,跟算法挺相似的,不过设计模式不是把算法应用到代码上,而是充分利用计算理论,直译器,编程语言特性的等等技术.
比如状态机和状态模式,直译器和直译器模式和访问者模式(访问者模式书上没有,不过
Python CookBook上有),OOP中对象系统的原型模型和原型模式等等都是证明,然而直译器模式需要你掌握直译器的技术才能明白,访问者模式需要你掌握匹配数据和递归的技术才能有所体会,这两个可能是设计模式中最难搞的两个模式,因为我读过一下
EOPL3,所以问题不大,而其它的设计模式不难.
其实几年前我也尝试去了解过设计模式,然而看书和大量的文章后还是一脸茫然,最后放弃了,现在看则是阔然开朗,我想表达的是学习设计模式更像是对你以前的开发经历的进行反省和总结.
所以我不推荐新人一上来就堆设计模式,等基础打得差不多并且有一定得实践经验再来学习,那样效果会更好,当然这段时间不用等太久,当熟悉一门编程语言得差不多就可以学了.
最后,不要看到别人说设计模式不好就不去学,一般有两种人会说不好,一种是给自己的懒惰找借口的,一种是已经学过并且了解到位的人,也就是说它不好的人很有可能是已经学了,学了永远比没学要好.
代码
由于代码可能经常更新,托管到 GitHub 上是一个很好的选择: https://github.com/saltb0rn/design-patterns
设计模式分类
设计模式可以分为三种:
- 创建模式 (Creational patterns): 处理对象的创建.
- 结构模式 (Structural patterns): 处理系统中实体(classes, objects and so on)之间的关系.
- 行为模式 (Behavioral patterns): 处理系统实体的通信交流.
创建模式
工厂模式 (The Factory Pattern)
工厂模式用于简化对象的创建.
抽象来看,工厂模式分为两个部分, client 和 factory . client 可以在不了解细节的情况下要求 factory 创建对象.
工厂方法 (Factory Method)
定义一个创建对象的接口,让它根据参数信息决定实例化哪一个类.接口可以是函数,也可以是类的方法.
假设现在要实现一个可以切换语言的客户端,每一种语言的翻译器都不一样,客户端只需要知道语言码就可以了.
抽象工厂 (Abstract Factory)
抽象工厂就是一组工厂方法的组合,用来产生一组产品相关或者相互依赖的对象,创建过程中不需要指定各种具体的类.
具体做法就是定义一个接口根据客户端的要求创建对象,而这个要求本身就暗示着一堆信息.
当创建一个对象有多个参数需要配置的时候可以使用这个模式.比如游戏主机的生产,各种硬件参数.
建造者模式 (The Builder Pattern)
当创建一个对象要求先创建多个其它类的对象的时候,可以采用这个模式.
有两个参与者, builder 和 director
跟工厂模式的抽象工厂类似,与工厂模式不同在于,工厂模式是一步创建好对象,而建造者模式是多步创建,并且创建过程需要 director 来指导.
比如现在模拟指挥者(director)指挥建筑工人(builder)建某种风格的房子.
原型模式 (The Prototype Pattern)
简单点就是通过克隆指定对象来创建新的对象,具体实现手段可以用深拷贝或者序列化与反序列化.
结构模式
适配器模式 (The Adapter Pattern)
适配器模式(Adapter)用于兼容两个不兼容的接口.具体实现细就是给跟系统所使用接口套一层同名的函数/方法.
比如有原本只有人和人可以交流沟通(系统),现在多了两个非人的对象(不兼容的对象),电脑和 GOOGLE 搜索引擎也想可人类交流.
装饰器模式 (The Decorator Pattern)
装饰器模式其实就是面向切面编程(Aspect Oriented Programming)范式,在不修改原有功能的基础上进行拓展.
在 OOP 中可以通过类继承的 override 或者 augment 两种手段进行拓展方法,或者给类添加方法来拓展类.
在 FP 中可以通过组合函数来拓展,其中 Common Lisp, Emacs Lisp, Python 这三门是我接触过的,对组合支持比较好的语言,
Python 装饰器的本质是基于现有函数的定义进行拓展,然后 mock .
外观模式 (The Facade Pattern)
Facade 是基于现有的复杂系统的一层抽象.
比如电脑启动时候的一些列动作,加载内存,读取 BOOT 地址,读取 BOOT 区域等等一系列的复杂动作,
作为用户只需要按下电源键就可以在不知道细节的情况下启动电脑了.电脑隐藏了这些细节,所以它就是 Facade .
享元模式 (The Flyweight Pattern)
该模式是用来提高性能的和内存的利用率.类也是一种数据结构,每生成一个对象就是在分配一次内存,
多个类似的对象所包含的数据可以通过共享来减少内存的使用.
一个对象根据需求可以分为两部份,状态无关并且不可变的数据(也叫固有数据)以及状态有关以及可变的数据.
固有数据部分可以划分为一个类,这个类叫做 flyweight .享元模式可以理解为于缓存(caching).
当需要大量创建对象的时候可以使用这种模式.
比如 FPS 游戏中,每个敌人大体差不多,每个敌人有自己的生命值,护甲属性等等,现在要大量生成敌人.
MVC模式 (The Model-View-Controller Pattern)
MVC 其实不算一种模式,它被认为是一种架构模式,而不是设计模式,前者的范围比后者的大很多.
它把一个引用分为 model, view 和 controller 3个组件.把Soc(Separation of concerns/关注分离)原则应用到 OOP 上.
Model代表信息集合体(knowledge),包含和管理逻辑,数据,状态以及应用的规则,是核心组件.View是model的可视表示,比如电脑的GUI,终端的文本输出,智能手机应用的GUI, 一个PDF文档,等等.View只负责展示数据,不处理数据.Controller负责连接/粘合view合model.view和model通过controller通信.
代理模式 (The Proxy Pattern)
为对象提供一个层代理,访问对象前要先通过代理.
代理有4种类型:
- 远程代理(remote proxy),为本地对象或者网络对象提供操作接口,隐藏背后的网络连接细节,无须意识到本地与网络对象的差别,比如
RPC,ORM. - 虚拟代理(virtual proxy),使用惰性初始化(lazy initialization)推迟高费用的对象的创建,直到真正需要创建的时候才创建.
- 保护代理(protection/protective proxy),对敏感对象的访问进行控制.
- 智能代理(smart/reference proxy),当访问对象的时候执行额外动作,比如引用计数和线程安全检测.
现在以实现保护代理为例子.
行为模式
责任链模式 (Chain of Responsibility Pattern)
当我们想让多个对象处理处理它们满足的请求,或者我们不能提前知道哪个对象能够处理这个请求,可以使用这个模式来处理请求.
最常见的例子就是 Web 的事件流,比如触发某个元素的点击事件,会先从顶级元素 html 开始向下传播事件,
直到找到目标元素并且执行点击事件,然后从该元素开始往 html 元素方向传播,如果传播经过的元素的点击事件被设定了就执行它.
比如现在模拟一系列检测点做为例子,从第一个点检查请求,然后自动一直检测到最后一个点.
命令模式 (The Command Pattern)
用于把一个命令/操作/动作封装成一个对象,创建一个包含所有所需要的逻辑和方法的类.
比如把修改文件名字做为一个操作,对它进行封装,其中可能需要在修改错误的时候撤销操作,那么就需要把撤销的操作做为一个部分也封装进去.
除了配套撤销之外,还可以实现 copy,cut 这样操作;可以记录命令等等,方便以后查询或者撤回.
直译器模式 (The Interpreter Pattern)
当需要给用户提供一门 DSL 的时候就需要用到这个模式,在 OOP 中就是把 AST 的节点换成类.就是直译器的写法.
观察者模式 (The Observer Pattern)
观察者模式描述了一个目标"发布者"和多个目标"订阅者"的发布-订阅关系.
"订阅者"依赖于"发布者",当"发布者"更新"订阅者"也会得到更新通知,这时候可以采用这种模式.
其实 MVC 就是从观察者模式延伸出来的.
状态模式 (The State Pattern)
该模式就是把状态机应用在 OOP 上,状态机的详细资料可以读 Theory Of Computation 这本书的第二章.
简单来说就是一件事物/系统在不同时间点可以有不同状态,每个时间点只能有一个状态,并且状态之间可以切换,这就是状态机.
比如格斗游戏里面的连招系统就是用了状态机,每个招式需要在合适的时间内按照要求顺序输入正确的指令完成,每一个指令就相当于一个状态.
如收音机也是一个例子,它有两个种操作,在 AM 和 FM 状态之间切换,在 AM 或者 FM 下切换电台.
策略模式 (The Strategy Pattern)
策略模式提倡使用多种算法解决一个问题,它的杀手特性(kill feature)是在运行时切换算法.
比如判断字符在字符串里面是否唯一,有两种套算法:
- 先给字符串排序,然后相邻字符一对一对地比较,如果两者相同就不是唯一.
- 把字符串转成
set(集合),如果字符串地长度和set元素数量不一样,就不是唯一.
明显,好字符串长的话应该交给第二套算法处理,其实不管哪种情况都是可以交给第二套算法来处理的,假设现在第二套算法处理短字符串效率不及算法一,据字符串长度来决定使用哪种算法.
具体做法就是把算法封装起来,在支持 FP 的语言中可以是函数,不支持 FP 但支持 OOP 可以封装成对象.
模板模式 (The Template Pattern)
模板模式专注于消除代码重复,如果两个/多个算法有重复的代码和相似的结果,可以把相同,不变的部分留在模板方法/函数(template method/function)里面,并且把可变,
不同的部分放到动作/钩子方法/函数(action/hook method/function).
可变,不同的部分之所以要放在方法/函数里面是因为语言设计本身采用了应用序(Applicative-Order)这种先计算参数的方式,所以不能插入代码,只能把函数或者类做为参数来实现惰性计算.
如果是 Lisp 可以采用 macro 来实现这种模式, macro 可以像正则序(Normal-Order)那样先展开代码后运算,是真正意义上的模板.
比如现在想要实现一个 banner 生成器,唯一变的就是样式.