设计模式思考
我对23种设计模式的理解:
类模式和对象模式
有些博文会把对象分为类模式和对象模式,在我看来这两种模式是可以相互转化的。类模式主要强调类的继承决定结构或者行为,是通过继承的延迟到子类去实现,来完成模式。而对象模式则主要强调对象的关联来完成模式(关联一个接口用桥接模式完成继承,关联一个对象用适配器模式完成调用)。所以所有的类模式都可以转化为对象模式,而对象模式则不一定可以转化为类模式。
对写golang的同学来说这一点很容易理解,因为golang只有关联没有继承。想要通过继承来延迟到子类去实现,实际上子类是通过关联一个实现了某个接口的对象以及父类,然后把关联的过程封装在创建过程中。这样不同的子类通过动态的组合不同的对象来完成继承。(实际就是桥接模式)
创建型(5种)
工厂
分为简单工厂模式和工厂模式
简单工厂模式:
最常用的模式,例如操作元数据,可能是Mysql可能是mongoDB,那么把Query动作封装成接口MetaI,根据配置文件选择来创建哪一个Meta。这种用法经常和单例配合使用。
工厂模式:
较常用的模式,是简单工厂模式的抽象,把具体的创建算法做成FactoryI。
对简单工厂模式的Meta例子来说,如果要新加一个Hbase,不需要修改已有的Factory静态方法,只需要添加一个HbaseFactory,然后指定这个Factory即可,这样满足了对扩展开放,对修改封闭的特性。(我因为偷懒都使用简单工厂模式来解决)
但是对设计框架来说,这就是不得不使用的设计模式了。因为不知道会被扩展的某个Product是具体哪个对象,只能用模版模式定义了在哪儿使用FactoryI来创建ProductI,才可以直接使用ProductI的方法。具体的实现通过继承延迟到子类去实现。
抽象工厂
我没用过这个模式,抽象工厂主要是解决产品族问题的。如果要造一辆奔驰车,用简单工厂模式去写,就是创建一个轮子厂生产奔驰轮子,再创建一个喇叭厂生产奔驰喇叭。如果奔驰车必须使用奔驰轮子,那为了不使客户端调用错误,就把这一部分逻辑抽象起来。
单例
很常用的模式,对微服务代码来说,log,config,元数据都会做成单例模式方便调用
建造者
从来没用过这种模式,和工厂模式很像。我认为算是工厂模式的扩展。
原型(很可能理解有误)
从来没用过,但是看一些博文有点像对象池技术。都是创建完了以后会生产一个缓存,再调用的话直接返回被缓存的对象。
结构型(7种)
代理模式的一般情况
代理
代理模式我认为算是适配器模式的特殊情况(不改变接口名)
也算是装饰模式的特殊情况(不添加额外功能)
更算是外观模式的特殊情况(只管一个子系统)
一般是用作缓存真实对象的数据,或者记录额外的日志这种对用户隐性的操作。
适配器
在我的经验中,当新的系统想要调用旧的接口,或者是预估将会于大量的第三方接口交互,这些接口可能有不同的请求协议。此时任何的三方协议会通过Adaptee接口进行转换填充Adapter字段,在系统内的逻辑只会操作适配类Adapter的成员。
装饰
当某个对象是通过接口访问的时候,可以通过关联这个对象,并且实现该对象的方法。然后继续通过接口访问来动态的给某个对象添加或者删除一些功能。
1 | a = addSomething(a) //添加功能 |
外观
外观模式通过直接持有子系统的类来简化外部访问的目的。
我从来没用过这个模式,但是从服务架构来说,现在的微服务架构总是有个接入层,封装了所有子系统的地址和调用流程等等。
桥接
当一个类存在两个变化纬度的时候,例如一个模型有3种形状,4种颜色,如果完全用多重继承去做的话,需要12个类。而桥接模式则是利用关联接口的方式来进行动态扩展。
只要一个类中结合了一个接口,就可以认做是单纬度的桥接模式,所以这种模式在golang中很常见。
组合
简单元素和复杂元素,或者子元素和父元素在操作上都是一样的接口,方便遍历访问和操作。
这个模式在设计各种树模式上很常见。
享元(可能理解有误)
一般结合工厂模式来使用,就是对象池了(怎么有点像原型模式?)
行为型(11种)
父类子类
策略
定义某个算法的框架,通过关联接口或者实际对象来动态选取不同策略,相当于对象模式
模版
定义某个算法框架,通过继承选取不同策略,相当于类模式
两类之间
观察者
订阅者和被订阅者的关系,当被订阅者发生变化的时候,订阅者都能收到通知。
当用广播或者数据库表之间有联动关系需要批量更新时很常见。
迭代器
结合组合模式使用来进行遍历
责任链
让每个处理模块保存下一个处理模块,从而链式或者树形对请求进行处理。
在请求特别复杂的服务中可以见到这种使用,例如在接入层时,一个请求特别复杂就需要不同的类串在一起一步步处理。
在adx/dsp系统中就是这样,这个模式我认为最大的缺点就是不容易debug,因为任何类都有可能做了某些操作影响下一个类。
命令
将发送者和接受者解耦,发送者不需要知道谁是接受者,只需要信息知道谁是接受者即可。
GoF的定义之一是,回调函数的面向对象版本
在我写过的代码中,最简单的流程是当接受到命令以后,入口逻辑通过switch分发逻辑到各个函数。而使用命令模式以后,每个switch都封装成命令,这样入口逻辑其实是不知道具体调用谁,只需要生成不同的消息对象,由消息类决定下一层(甚至可以没有下一层)。比起原来的逻辑来说,有命令的修改就会更为灵活。
下面这一篇文章从游戏的角度来分析命令模式和其扩展应用,写的很好
游戏设计模式(二) 论撤消重做、回放系统的优雅实现:命令模式
当指令全部封装成命令以后,方便做日志回放(存储系统中很常见,mysql,redis等等)。
下面这一篇文章很有意思
有些想法是我赞成,确实很多语言特性使得有些设计模式不需要再使用了。但是使用回调和闭包去代替命令模式,岂不是走回头路。
类实例是绑定了行为的数据,闭包是绑定了数据的行为
闭包相当于使用了静态变量的命令模式。大量的回调闭包使得js成为了回调地狱,因而专门出现了promise的写法,然而es6开始也出现了类的概念,总是要面向对象和设计模式化才能使得项目易于多人维护的。
备忘录
将一些成员变量打包成类保存起来,存到另外一个备忘录列表类中。在桌面编辑软件中很常见,用来做redo。
这个模式我基本没用过,一般有这种需求的也只是存在当前类的另外一个变量中,很少单独存了放到类外。一般也只需恢复一个版本即可。
状态
当对象的状态改变时,同时改变其行为(调用状态挂钩的回调函数)
很常用的模式,不过状态维护不好很容易吐血。一般3到4个状态我会尝试用状态模式,再多就很容易出bug了。(新增一个状态是需要修改其他状态的逻辑的,这种设计模式对修改开放容易出bug)
通过中间类
访问者(理解不深刻)
从来没有用过这个模式,由于golang没有重载,因此实现起来会有一些不同
1 | package main |
对于数据结构稳定,但是算法可能有很多种的情况下(这样的情况确实很少见,一般都是数据结构多变)
上面实现中的RealVisitor可以很灵活的替换为其他的Visitor实现。
(我感觉,如果把visitor接口内置在animal类里,这种组合方式就变成了单维接口但是多个方法的桥接模式,所以如果我遇到这种场合,第一反应应该是这么做)
中介者
基本每个人有每个人理解的类图。
但是核心思想是一样的,当多个类之间相互依赖的时候。往往一个类的代码改动会需要调整另外一个类的代码,因此把类的交互抽象成中介者类,相互影响的逻辑都封装在中介者类中。
从而避免了网状结构,变成星状结构
=>
解释器
一般如果不是语法分析很少会用到这种模式。
相似的设计模式
工厂模式和建造者模式
工厂模式分为main,Product,ConcreteProduct,Factory,ConCreteFactory
建造者模式分为main,Product,ConcreteProduct,Builder,ConcreteBuilder,Director
使用上main都是利用Product结合策略方法去使用,但是建造者模式把Factory分拆成了Builder和Director。
把粗线条的构造一个对象变成了精细的结构,如果在使用工厂模式时,对象的组成有类似可以抽象的结构,就可以转化为建造者模式去表达。
例如车对象构造时需要轮子和喇叭。最初用工厂模式来写,这一部分都在ConCreteFactory代码中。后来发现需要具体关心这一部分,把不变的组合逻辑抽象出来,就可以转化为建造者模式。这样就可以设置不同的轮子和喇叭。
一句话概括就是建造者模式需要更加精细的关心对象的组成部分
适配器,装饰,外观和代理模式
上面提起过,代理模式是其他模式的特殊情况,完全可以被其他模式所替代。相当于适配器模式不改变接口名,相当于装饰模式不添加额外功能,相当于外观模式只管一个子系统(另外外观模式不涉及任何的抽象而是直接操作实体)
外观模式直接操作实体对象因此很容易和其他模式进行区分
适配器模式和装饰模式的区别在于:适配器模式会改变接口名但是不改变逻辑,而装饰模式不改变接口名但是会添加额外逻辑。