我们知道设计模式并不是为了介绍某种技术或者设计方法,也不会要求开发者使用某种编程技巧,因为设计模式并不立足于某种语法特性中。设计模式仅仅只是描述了在面向对象软件设计中针对于某一类问题而提出的解决方案,这些解决方法已经在前人的艰苦努力中得到充分的成功实践。
为了解决一个又一个的实际问题,前人在不断的总结和实践中为我们绘制出一幅幅的方案蓝图,蓝图中就包含了每种模式的类图结构。但不幸的是,对设计模式并不熟悉的开发者来说,有一些模式在类图结构上表现出了高度的相似性,以至于我们总是产生疑惑:这两个模式看起来差不多呀,他们究竟有何区别呢?
在列举多个模式之间的区别之前,我总结了一些甄别两个模式的依据,我们可以按照这些依据尝试将两个模式区别开来。请注意,以上依据仅是我个人的经验总结,如果你对此有不同意见或者有更多的方法可以在 issue 中告诉我。
(1)-目的是鉴别模式的唯一标准
在前面我们提到,设计模式描述了针对于某一类特定问题而提出的解决方案,所以理解模式之前得充分融入进该模式所面临的场景。不了解实际面临的问题,即便你把该模式的内容全篇背诵下来也没有任何作用,所以我们在讨论每一个模式时,都单独提供了一小节内容来讨论意图。如果我们在学习的过程中,发现有两个模式在类图结构上表现的高度一致,目的就成了鉴别他们的唯一依据。换句话说,两个模式在解决问题的过程中,恰巧表现出来同样的类图结构,但他们的出发点却是完全不同的!
但遗憾的是,很多时候我们无法利用这一条依据准则,尤其是在阅读各种框架源码时,要猜测作者出于何种目的来组织类间关系是不容易的。如果作者使用了规范的类的命名,鉴别则变得很容易。
(2)-规范的命名很重要
规范的命名可以使得读者不必了解所有的类间关系就能清楚的知道代码中使用了哪一种模式。如果源码的作者在类命名时遵循了一定的规范,比如建造者类使用Builder
结尾,策略类使用Strategy
结尾,这会大大的提升源码的可阅读性。所以规范的命名很重要,如果你是读者,可以从规范的命名中受益,如果你是作者,也应遵循规范,以此减轻其他读者的阅读障碍。
(3)-不同的侧重点可能隐藏着不同的细节
尽管有些模式在结构上表现的极其相似,但这并不意味着他们一模一样,了解他们在结构上的细小差别 不仅能帮助更好的掌握他们,也是区分他们的切入点。关于这一点,在后续的模式对比中,我们将一组一组的分析。
组合模式和装饰器模式的通用类图结构如上图所示,从类图组成上看,他们均包含有抽象组件Component
、具体组件(ConcreteComponent
、Leaf
)和聚合对象(Composite
、Decorator
)的角色。从功能上看,他们都能实现对象的递归组合。当装饰器模式中装饰器仅有一个时,我们可以省略抽象的装饰器,此时装饰器模式和组合模式的类图几乎一模一样。
尽管如此,他们还是在某些方面表现出各自的特性。从意图来看,装饰器模式依靠层层包装的机制,动态的给原始对象增加一些职责,每个装饰器对象内部仅维护一个组件(或者包装器)对象;组合模式的主要目的是为了表示部分-整体的树形关系,每一个组合对象内部均可维护多个叶子节点(或者组合对象),相较于装饰器模式来说,组合模式的侧重点是对象的聚集(比如获取子节点列表的行为Component#getChildren()
、添加子节点的行为Component#add(Component)
),是如何将一系列对象组织成一个树形结构,以便我们可以像处理单个对象一样来处理多个组合的对象。我们可以武断的认为装饰器模式是退化的(不注重对象的聚集)、内部仅维护一个组件的组合模式,但你必须牢记他们的出发点完全不一样。
组合模式与解释器模式的结构差不多,这并不是巧合,事实上,我们可以认为解释器模式中的某些部分是组合模式的实例。在上面我们提到组合模式是为了将一系列对象聚集成树形结构,以此来表示“部分-整体”的关系。譬如汽车由发动机、轮胎、车座等部件组成,而发动机又由燃料系统、启动系统、点火系统和冷却系统等组成,此时所有的对象就组成了一棵树。如果汽车代表“整体”,那么发动机、轮胎、车座等部件就是“部分”;如果发动机作为“整体”,那么燃料系统、启动系统等才是“部分”。
而解释器模式将一个复杂的完整表达式的求解过程拆分到一个个简单的解释器对象中,通过这些简单的对象便能完成求解。例如x+y
这样的的一个表达式,对于表示符号+
的NonterminalExpression
对象来说,必定维护了一个表示x
的表达式引用和一个表示y
的表达式引用,而他只需要完成前后两部分最终值的求和即可。正如我们在解释器模式一章中所说,一个完整的表达式最终会被解析成为一个抽象语法树对象,从结果上来讲,这些简单的解释器对象在无形之中就组成了一个树形结构。
尽管组合模式和解释器模式出于不同的出发点考虑,却殊途同归的呈现出来同样的结构。从这个角度来说,我们可以认为解释器模式中的抽象语法树是组合模式的一个实例。注意,这并不是说解释器模式是抽象语法树的一个实例,我们仅仅指的是解释器模式中的抽象语法树(除环境类Context
之外的部分)。
但从侧重点的角度上来说,他们就完全不一样,我们构建抽象语法树的目的是为了解释一个复杂的句子,语法树中的每个对象都有解释自身的操作,所以解释器模式关心的是解释这一行为。但组合模式将更多的精力投入到对象的组织上,所以组合模式属于结构型模式,而解释器模式属于行为型模式。
中介者模式和门面模式都通过增加一个中间对象来对系统解耦,中介者模式通过增加中介者对象Mediator
来封装多个相互依赖的对象之间的交互,门面模式则提供了门面对象Facade
来降低客户端在使用子系统时的复杂程度。
中介者模式和门面模式所强调的侧重点不一样,中介者模式引入中介对象能解除原本错综复杂的依赖关系,中介者对象旨在封装交互,属于行为型模式;而门面模式能让客户端(在大多数情况下)只需要和门面对象打交道,强调对系统内部复杂的结构的封装,属于结构型模式。
另外,中介者模式和门面模式的使用场景有所区别,门面模式致力于如何屏蔽系统内部的复杂性,让外部客户端使用起来更简单;而中介者模式则是为了解决系统内部的问题,每一个Colleague
对象都有可能是中介者模式的客户端。
桥模式和抽象工厂模式本来没有共性,桥模式专注于分离抽象部分和实现部分,实际上我们所指的抽象部分和实现部分指的是对象的多个维度,关于这一点在《多个维度之间的解耦 —— 桥模式》一篇中已经详细讨论过,此处不再赘述。而抽象工厂的目的在于创建一系列产品,并且更重要的是每组产品之间是相关或者相互依赖的。
看起来,桥模式和抽象工厂模式都在讨论一个对象的多个维度,但他们的适用场景并不一致。简单来说,桥模式对于多个维度的搭配按照数学中的笛卡尔积的方式进行,而抽象工厂模式则是固定搭配。
场景一:当一个图形被绘制到屏幕上时,描述一个图像的特征除了形状或许还有颜色,理论上,我们希望形状和颜色可以任意搭配。如果颜色有红黄蓝,形状有矩形和圆形,那么可能的情况有红矩形、红圆形、黄矩形、黄圆形、蓝矩形、蓝圆形 6 种搭配。因为任意一种形状都可和任意一种颜色进行搭配,适合使用桥模式来构建系统。
场景二:考虑一个支持多种序列化方式(JDK、Json、Xml、ProtoBuf、Hessian)的程序,对于任意一个序列化器来说,一定有一种与之匹配的反序列化器实现。如果我们使用 Xml 的反序列化器来反序列化 Json 序列化器序列化的内容时,程序定然不会正常完成反序列化的工作。此处序列化器和反序列化器之间存在一一对应关系,采用抽象工厂模式构建更为恰当。
除上述的几组模式对比之外,在每一种类型的模式总结篇中,我们列举了更多模式之间的对比情况。更多内容请访问关于行为型模式的讨论、关于创建型模式的讨论和关于结构型模式的讨论。