设计模式思考

我对23种设计模式的理解:

类模式和对象模式

有些博文会把对象分为类模式和对象模式,在我看来这两种模式是可以相互转化的。类模式主要强调类的继承决定结构或者行为,是通过继承的延迟到子类去实现,来完成模式。而对象模式则主要强调对象的关联来完成模式(关联一个接口用桥接模式完成继承,关联一个对象用适配器模式完成调用)。所以所有的类模式都可以转化为对象模式,而对象模式则不一定可以转化为类模式。

对写golang的同学来说这一点很容易理解,因为golang只有关联没有继承。想要通过继承来延迟到子类去实现,实际上子类是通过关联一个实现了某个接口的对象以及父类,然后把关联的过程封装在创建过程中。这样不同的子类通过动态的组合不同的对象来完成继承。(实际就是桥接模式)

创建型(5种)

工厂

分为简单工厂模式和工厂模式

简单工厂模式:
1

最常用的模式,例如操作元数据,可能是Mysql可能是mongoDB,那么把Query动作封装成接口MetaI,根据配置文件选择来创建哪一个Meta。这种用法经常和单例配合使用。

工厂模式:
1

较常用的模式,是简单工厂模式的抽象,把具体的创建算法做成FactoryI。

对简单工厂模式的Meta例子来说,如果要新加一个Hbase,不需要修改已有的Factory静态方法,只需要添加一个HbaseFactory,然后指定这个Factory即可,这样满足了对扩展开放,对修改封闭的特性。(我因为偷懒都使用简单工厂模式来解决)

但是对设计框架来说,这就是不得不使用的设计模式了。因为不知道会被扩展的某个Product是具体哪个对象,只能用模版模式定义了在哪儿使用FactoryI来创建ProductI,才可以直接使用ProductI的方法。具体的实现通过继承延迟到子类去实现。

抽象工厂

1

我没用过这个模式,抽象工厂主要是解决产品族问题的。如果要造一辆奔驰车,用简单工厂模式去写,就是创建一个轮子厂生产奔驰轮子,再创建一个喇叭厂生产奔驰喇叭。如果奔驰车必须使用奔驰轮子,那为了不使客户端调用错误,就把这一部分逻辑抽象起来。

单例

很常用的模式,对微服务代码来说,log,config,元数据都会做成单例模式方便调用

建造者

1

建造者模式

从来没用过这种模式,和工厂模式很像。我认为算是工厂模式的扩展。

原型(很可能理解有误)

从来没用过,但是看一些博文有点像对象池技术。都是创建完了以后会生产一个缓存,再调用的话直接返回被缓存的对象。

结构型(7种)

代理模式的一般情况

代理
1

代理模式我认为算是适配器模式的特殊情况(不改变接口名)

也算是装饰模式的特殊情况(不添加额外功能)

更算是外观模式的特殊情况(只管一个子系统)

一般是用作缓存真实对象的数据,或者记录额外的日志这种对用户隐性的操作。

适配器
1

在我的经验中,当新的系统想要调用旧的接口,或者是预估将会于大量的第三方接口交互,这些接口可能有不同的请求协议。此时任何的三方协议会通过Adaptee接口进行转换填充Adapter字段,在系统内的逻辑只会操作适配类Adapter的成员。

装饰
1

当某个对象是通过接口访问的时候,可以通过关联这个对象,并且实现该对象的方法。然后继续通过接口访问来动态的给某个对象添加或者删除一些功能。

1
2
a = addSomething(a) //添加功能
a = getOriginal(a) //还原原来的对象
外观
1

外观模式通过直接持有子系统的类来简化外部访问的目的。

我从来没用过这个模式,但是从服务架构来说,现在的微服务架构总是有个接入层,封装了所有子系统的地址和调用流程等等。

桥接

当一个类存在两个变化纬度的时候,例如一个模型有3种形状,4种颜色,如果完全用多重继承去做的话,需要12个类。而桥接模式则是利用关联接口的方式来进行动态扩展。

只要一个类中结合了一个接口,就可以认做是单纬度的桥接模式,所以这种模式在golang中很常见。

组合

简单元素和复杂元素,或者子元素和父元素在操作上都是一样的接口,方便遍历访问和操作。

这个模式在设计各种树模式上很常见。

享元(可能理解有误)

一般结合工厂模式来使用,就是对象池了(怎么有点像原型模式?)

行为型(11种)

1

父类子类

策略

定义某个算法的框架,通过关联接口或者实际对象来动态选取不同策略,相当于对象模式

模版

定义某个算法框架,通过继承选取不同策略,相当于类模式

两类之间

观察者

订阅者和被订阅者的关系,当被订阅者发生变化的时候,订阅者都能收到通知。

当用广播或者数据库表之间有联动关系需要批量更新时很常见。

迭代器

结合组合模式使用来进行遍历

责任链
1

让每个处理模块保存下一个处理模块,从而链式或者树形对请求进行处理。

在请求特别复杂的服务中可以见到这种使用,例如在接入层时,一个请求特别复杂就需要不同的类串在一起一步步处理。

在adx/dsp系统中就是这样,这个模式我认为最大的缺点就是不容易debug,因为任何类都有可能做了某些操作影响下一个类。

命令
1

将发送者和接受者解耦,发送者不需要知道谁是接受者,只需要信息知道谁是接受者即可。

GoF的定义之一是,回调函数的面向对象版本

在我写过的代码中,最简单的流程是当接受到命令以后,入口逻辑通过switch分发逻辑到各个函数。而使用命令模式以后,每个switch都封装成命令,这样入口逻辑其实是不知道具体调用谁,只需要生成不同的消息对象,由消息类决定下一层(甚至可以没有下一层)。比起原来的逻辑来说,有命令的修改就会更为灵活。

下面这一篇文章从游戏的角度来分析命令模式和其扩展应用,写的很好

游戏设计模式(二) 论撤消重做、回放系统的优雅实现:命令模式

当指令全部封装成命令以后,方便做日志回放(存储系统中很常见,mysql,redis等等)。

下面这一篇文章很有意思

先让简单的命令模式消失吧

有些想法是我赞成,确实很多语言特性使得有些设计模式不需要再使用了。但是使用回调和闭包去代替命令模式,岂不是走回头路。

类实例是绑定了行为的数据,闭包是绑定了数据的行为

闭包相当于使用了静态变量的命令模式。大量的回调闭包使得js成为了回调地狱,因而专门出现了promise的写法,然而es6开始也出现了类的概念,总是要面向对象和设计模式化才能使得项目易于多人维护的。

备忘录
1

将一些成员变量打包成类保存起来,存到另外一个备忘录列表类中。在桌面编辑软件中很常见,用来做redo。

这个模式我基本没用过,一般有这种需求的也只是存在当前类的另外一个变量中,很少单独存了放到类外。一般也只需恢复一个版本即可。

状态
1

当对象的状态改变时,同时改变其行为(调用状态挂钩的回调函数)

很常用的模式,不过状态维护不好很容易吐血。一般3到4个状态我会尝试用状态模式,再多就很容易出bug了。(新增一个状态是需要修改其他状态的逻辑的,这种设计模式对修改开放容易出bug)

通过中间类

访问者(理解不深刻)

类型识别

java访问者模式

C++访问者模式

访问者模式讨论篇:java的动态绑定与双分派

从来没有用过这个模式,由于golang没有重载,因此实现起来会有一些不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
)

type animal interface {
accept(visitor)
}

type Dog struct {
name string
}

func (this *Dog) accept(v visitor) {
v.methodDog(this)
}

type Cat struct {
name string
}

func (this *Cat) accept(v visitor) {
v.methodCat(this)
}

type Animal struct {
d Dog
c Cat
}

func (this *Animal) accept(v visitor) {
v.methodCat(&this.c)
v.methodDog(&this.d)
}

type visitor interface {
methodDog(*Dog)
methodCat(*Cat)
}

type RealVisitor struct{}

func (this *RealVisitor) methodDog(d *Dog) {
fmt.Println(d.name)
}

func (this *RealVisitor) methodCat(c *Cat) {
fmt.Println(c.name)
}

func main() {
var a Animal
a.c.name = "Cat"
a.d.name = "Dog"

v := &RealVisitor{}
a.accept(v)
}

对于数据结构稳定,但是算法可能有很多种的情况下(这样的情况确实很少见,一般都是数据结构多变)

上面实现中的RealVisitor可以很灵活的替换为其他的Visitor实现。

(我感觉,如果把visitor接口内置在animal类里,这种组合方式就变成了单维接口但是多个方法的桥接模式,所以如果我遇到这种场合,第一反应应该是这么做)

中介者

基本每个人有每个人理解的类图。

但是核心思想是一样的,当多个类之间相互依赖的时候。往往一个类的代码改动会需要调整另外一个类的代码,因此把类的交互抽象成中介者类,相互影响的逻辑都封装在中介者类中。

从而避免了网状结构,变成星状结构

1

=>

1

图来源

解释器

一般如果不是语法分析很少会用到这种模式。

相似的设计模式

工厂模式和建造者模式

工厂模式VS建造者模式

工厂模式分为main,Product,ConcreteProduct,Factory,ConCreteFactory

建造者模式分为main,Product,ConcreteProduct,Builder,ConcreteBuilder,Director

使用上main都是利用Product结合策略方法去使用,但是建造者模式把Factory分拆成了Builder和Director。

把粗线条的构造一个对象变成了精细的结构,如果在使用工厂模式时,对象的组成有类似可以抽象的结构,就可以转化为建造者模式去表达。

例如车对象构造时需要轮子和喇叭。最初用工厂模式来写,这一部分都在ConCreteFactory代码中。后来发现需要具体关心这一部分,把不变的组合逻辑抽象出来,就可以转化为建造者模式。这样就可以设置不同的轮子和喇叭。

一句话概括就是建造者模式需要更加精细的关心对象的组成部分

适配器,装饰,外观和代理模式

上面提起过,代理模式是其他模式的特殊情况,完全可以被其他模式所替代。相当于适配器模式不改变接口名,相当于装饰模式不添加额外功能,相当于外观模式只管一个子系统(另外外观模式不涉及任何的抽象而是直接操作实体)

外观模式直接操作实体对象因此很容易和其他模式进行区分

适配器模式和装饰模式的区别在于:适配器模式会改变接口名但是不改变逻辑,而装饰模式不改变接口名但是会添加额外逻辑。

参考资料

设计模式-菜鸟教程

那些相似设计模式的区别

23种设计模式全解析