设计模式进一步解读
一. 概述
设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
之前有整理一份有关代码开发篇之设计模式的简单介绍,但并没有总结。因此借此机会,重新梳理并期望得到最本质的东西,而无需记这些概念。
面向对象设计原则有七大原则,这些设计模式都是由这个原则衍生出来的具体场景。
单一职责 一个只负责一个功能领域中的相应职责。强调的功能具有原子性,不能在分解出再细小的功能。
开闭原则 软件实体应对扩展开发,而对修改关闭。强调的是不建议修改现有的代码。
里氏代换原则 所有引用基类对象的地方能够透明使用其子类的对象,这里使用面向对象中的-多态这个特性。
依赖倒转原则 抽象不应该依赖于细节,细节应该依赖于抽象。这里使用面向对象中的继承,多态这两个特性。可以理解为抽象编程或者接口编程。
接口隔离原则 使用多个专门的接口,而不是使用单一的总接口。强调的是接口的独立性。
合成复用原则 尽量使用对象组合,而不是继承来达到复用的目的。强调的是尽量通过对象组合方式而不是继承方式。
迪米特法则 一个软件实体应当尽可能少地与其他实体发生相互作用。强调的是减少耦合性,一句名言《计算机中的任何问题,都可以通过加一层来解决》,感兴趣的可以拜读张建飞的《程序员的底层思维》。
这七大原则表达的是设计时,功能模块(方法)尽量原子性和独立性,同时类似功能模块下做抽象化或接口化,接着运用面向对象语言的特性(多态,继承,方法重载等),来减少代码中的 if 代码,从而满足开闭原则。然而过多的层级继承会导致代码的复杂度,所以尽量通过对象与对象之间的组合方式,而这个组合方式由中间类(对象)充当桥梁,来减少继承复杂度,降低耦合。
二. 代理模式 &适配器模式
代理模式和适配器模式有点类似,都是通过一个类或者对象间接调用目标类或对象;
只是出发目的不同,代理模式是对目标类或对象提供一个门户(即代理类或对象),任何人不得直接访问目标类或对象。有点类似 java 中的“切面”这个概念;通过这个切面,任何访问目标类或对象的,都会执行切面的所包含的动作;
而适配器模式是对接口层面上的提供兼容性。例如目标接口中需要三个参数,而调用者可能只传递一个参数或者是两个参数。为此,需要提供一个适配对象进行包装起来;
三. 装饰模式 &组合模式 &观察则模式
装饰模式是对原功能(单一职能)进行了拓展,有点类似代理,但目的不同。
在 JDK 中我们可以看到类似的模式,例如 IO 操作;
InputStream 是抽象的输入流
FilterInputStream 是基于文件形式的输入流的实现类
FilterInputStream 是基于 InputStream 做的装饰模式下的抽象类。可以理解为上图的 Decorator
BufferedInputStream 是装饰模式下的具体实现类,拓展功能是将读取的信息流缓存起来;
DataInputStream 是装饰模式下的具体实现类,拓展功能是增加读取 java 各种原始数据类型的数据;
组合模式强调的是“整体-部分”关系的层次结构。
最经典的例子就是图形系统。下面是基于 java 中的 awt 图形子系统;
而观察者模式则强调的是事件-订阅的隔离,其类结构于组合模式有点类似;
在很多开源系统中都能看到其身影,在 spring 框架中例子如下:
之所以有些许差异,主要是增加了缓存功能,提供检索效率;
四. 桥接模式 &策略模式
桥接模式强调的是接口编程,将抽象部分与它的实现部分分离,使它们都可以都独立地变化。这块的代码随处可见,只要看到定义成接口的代码,基本上都可以认为使桥接模式;
策略模式也是接口编程,然而特殊之处是被调用方根本不知道该调用具体的实现类,而是由调用方来决定,其表现形式基本上都是方法参数中传递,也就是说调用方通过方法参数传递具体的实现类,由被调用自行去调用;
五. 职责链模式 &命令模式 &备忘录模式
5.1 职责链
职责链模式是以链路形式,经过链路上的各个节点处理。有点类似流水线;
上面的类图中 Handler 集合了两个功能,职责不够单一。有另一个版本,其通过另外一个类来承接将各个 Handler 关联起来,姑且称其为 HandlerContext。HandlerContext 的实现有两种方式,一种是数组,另外一种是链表。
在 tomcat 中的过滤器实现中,其类图关系如下:
其时序图如下:
而 netty 框架中却是采用链表的形式,其类图如下:
ChannelPipeline 是充当整个链路的门户,主要还是有 AbstractChannelHandlerContext 类来实现。这个类可以理解为外观模式。
AbstractChannelHandlerContext 是将 ChannelHandler 进行连接起来。
ChannelHandler 才是真正的动作,例如序列化,安全校验等动作;
另外,对 InBound 和 Outbound 的理解:InBound - 内部调用链;OutBound - 外部调用链入口;
有关 netty 这个职责链的时序图如下:
5.2 命令模式
命令模式强调的是发送请求(命令)与请求(命令)接收者(对命令进行对应的动作)完全解耦(可以理解为一个命令对应一个接收者),意味着它们俩之间需要一个对象来处理这个映射关系;
其结构与观察者模式有点类似,然而观察者模式是一对多的关系;其类图如下:
其时序流程如下:
在 RocketMQ 开源项目中可以看到其身影:
其时序图如下:
我们可以在执行 RequestProcessor 之前追加各种特性,例如日志打印,身份验证等,然而这些特性一般都是以职责链的形式来实现。由于 RocketMQ 是基于 Netty 网络框架实现的,这些特性可以在 Netty 框架中追加,而不用在 RocketMQ 自身上实现;
另外还有 bpmn-js 框架中使用的 diagram-js,忽略一些其他功能,将其主要的代码梳理出来,其类图如下:
其中 Command 主要是字符串,其值有 shape.append、shape.create、shape.delete、shape.move 等等。
其时序图如下:
5.3 备忘录模式
备忘录模式强调的是将对象的变化状态的历史记录下来,以便可以回滚到指定状态;这里涉及到三个动作,一个是对象状态的变化,另外两个分别是记录状态,回滚状态。为了符合单一职责,开闭原则,状态变化的记录以及回滚封装为一个独立模块,另外需要一个门户,将它们俩关联起来;其类图如下:
其时序图如下:
然而这样的结构图,不经常使用。一般会结合命令模式来实现;这个可以看上一个章节《命令模式》中的 diagram-js 框架的类图。命令(CommandHandler)中有 revert 动作。
根据 diagram-js 框架中的类图,有关 revert 动作的时序图:
六. 解释模式
定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。可以理解为语法树。大白话就是解释模式等同于语法树的代码体现;在 spring 框架中 EL 表达式的语法树,可以查阅 spring-el 包中的 SpelNode 接口以及其实现类。
七. 迭代器模式
迭代器模式在集合类最常见。其主要解决的是元素的存储与遍历职责拆分。
九. 状态模式
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
在整理这个状态模式时,发现不能没有理解这个状态模式:是为了解决不同状态下对应不同行为的映射呢,还是解决对象的各个状态流转以及不同状态下该对象行为的限制有所不同?
如果是解决不同状态下对应不同行为的映射,这个映射可以通过 Map 类来实现,或者是枚举类型;
如果是对象的各个状态流转以及不同状态下该对象行为的限制有所不同,那么可以将这些限制委托给状态类来处理,类图较为粗糙,如下图;
时序图如下:
由于状态的流转后续有可能会发生变更,因此可以采用表达式引擎来替代 if 判断,因此需要这么一个类 StateTransformEngine 充当这个角色。
十. 外观模式 &中介者模式
10.1 外观模式
外观模式可以理解为是一个门户,子系统入口由这个门户统一提供。或者是将复杂的操作通过这个门户简化后提供入口给调用方使用。所以,当有很多模块但又不想过于分散(直接调用这个模块接口),则可以将这些模块的访问全部交给门户,由它来提供提供入口;类图如下:
10.2 中介者模式
中介者模式强调的是各个对象相互有依赖,引用。为了避免耦合,由一个中介者对象充当这个桥梁。
十一. 访问者模式
访问者模式主要解决的是对对象结构中的各元素的访问操作。那么这里有几个关键点:
不同访问者对其所采取的访问操作不一样
对象中由各个元素组合而成
其简单的类图如下:
时序图大概如下:
在个别开源框架中可以看到其身影,例如 QueryDsl 中 com.querydsl.core.types.Visitor,还有 java 编译器(jdk.compiler)包中的 com.sun.tools.javac.tree.JCTree.Visitor。
由于其逻辑过于复杂,后续会针对开源框架中使用其模式进行详细介绍。
该模式是借用了编程语言中的方法重载这个特性来实现;
总结
在继续介绍剩余的模式,就有点冗余了。一方面剩余的设计模式不常见,在开发时使用到的可能性很低;另一方面可以借鉴的点与上面的模式都雷同,大差不差。
在我们开发过程中,尽量采用接口编程(抽象化)的思维去定义各个功能模块,功能模块尽量原子性,不要将多个原子性功能全部放在一起,而接口之间的关系采用中间类来解耦,而对于同一个接口有不同的实现方案,借用面向对象语言的特性处理。具体采用哪种方案,有调用者自行决定(策略模式)。
但是,好的代码不是一蹴而就的,毕竟在初期时对其认知还不太深入。例如,初期时设计这个接口就初浅的认为是原子性的,然而后面却发现却不是原子性的。
在开发业务需求时,无所避免的会添加各种判断(if 语句),这是可以暂停下来,看是否有其他方案来替换掉 if 语句;例如运用语言特性(多态,方法重载),又或者通过映射关系。
版权声明: 本文为 InfoQ 作者【邱学喆】的原创文章。
原文链接:【http://xie.infoq.cn/article/703d7190463c2af063ed18be8】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论