前文介绍了设计结构优良的代码应该遵循的设计原则和尽量避免的设计臭气。基于这些原则,四人帮(Gang of Four)抽象出一些常用的设计模式,用于解决在编程过程中反复遇到的设计问题。笔者认为“复用”是软件设计最具价值的理念之一,甚至有人提出软件即复用。通常我们认为复用是指代码层面的复用,然而笔者认为设计模式是设计思想的重用。本文将重点介绍常用的四个模式:Façade模式、Adapter模式、Strategy模式和Bridge模式。考虑到部分模式比较抽象难懂,笔者将在后面专门写一遍设计模式应用案例的文章。
一.Façade模式
1. 目标:希望简化原有系统繁琐的使用方式,并重新定义自己的接口。
2. 问题:只需要使用某个复杂系统的部分功能,或者需要以一种特殊的方式与系统进行交互。
3. 解决方案:Façade为原有系统的Client封装一个新的接口,供Client调用。
4. 参与者和协作者:接口、各个子系统。接口提供简化后的调用方式,子系统提供具体功能的实现。
5. 效果:简化对子系统的使用过程。但是Façade并不完备,因此客户可能无法使用被精简的功能。
6. 实现:定义一个(或多个)具备所需接口的新类,新的类将组合原系统中的具体实现类,实现所需功能。代码请参考06. 设计模式应用案例(上)。UML图如下:
上面UML图称为设计模型,是对设计方案的抽象,他抽象的是问题的解决方案。设计模式是对在某一特定语境下反复出现的一类问题的解决方案的抽象,可以理解为它是设计模型的抽象,更高一层。
二.Adapter模式
1. 目标:将一个难以控制的(如无法修改其内部代码)对象匹配到特定的接口上。从而将一个类的接口转换成客户希望的另一个接口。
2. 问题:已有系统拥有合适的数据和行为,但接口并不合新系统的要求。
3. 解决方案:Adapter面向所需的接口提供一个包装器,将原系统中合适的功能封装在Adapter的具体实现内。
4. 参与者和协作者:Adapter对Adaptee进行适配,使其满足新接口要求。这时用户就可以间接使用Adaptee已实现功能。
5. 效果:Adapter模式使得先前存在的对象可以匹配新的类型,而不受该对象原有接口的限制。从而实现原来由于接口不兼容而不能一起工作的类可以一起工作。
6. 实现:用一个满足现有接口需求的新类组合已有类(对象Adapter)或者继承已有类(类Adapter),调用已有类的方法实现新类中的方法。代码请参考06. 设计模式应用案例(上)。UML图如下:
a. 对象Adapter(推荐使用):
b.类Adapter:慎重使用,容易污染新类,引入不需要的行为。
三.Strategy模式
1. 目标:根据不同的语境动态的配置业务规则或算法,无需修改现有逻辑。
2. 问题:在实际中我们经常会发现一个问题有多种可选的策略,这些策略在概念上具有相同的功能,但是应用于不同的场景。抽象为:对于某种算法的选择依赖于用户的请求内容或算法所使用的数据。如果某处的算法始终不会变化,则没有必要使用Strategy模式。
3. 解决方案:将算法的实现和选择相分离。允许根据语境来动态选择合适的算法。需要注意的是,如果可选算法的种类或者具体算法的实现都是动态变化的,那么策略模式不是最佳的解决方案,因为每次修改都会导致源代码的变更。这种情况,采用规则引擎来处理将更加合适,规则引擎可实现算法逻辑的动态添加和修改。
4. 参与者和协作者:Strategy给出了应如何使用各种不同算法的信息;ConcereStrategy实现了这些不同的算法。Context通过一个Strategy类型的引用使用特定类型的ConcrereStrategy。
5. 效果:Strategy定义一族算法,减少Switch和条件分支语境(if else)的使用,所有的算法必须用同样的接口进行调用,运行时通过多态选择合适的算法实现。
6. 实现:由使用算法的类Context类包含一个抽象类Strategy,Strategy类有一个抽象方法决定如何调用算法,每一派生类实现一个需要的算法。如果简单的使用继承关系来对这些策略上的差异进行建模,可能会导致:类的个数迅速失控(类的个数随着变化以乘法数增加,设计模式很多时候就是把乘法变加法),代码大量重复、冗余,复用无法进行。代码请参考06. 设计模式应用案例(上)。UML图如下:
四.Bridge模式
1. 目标:将一组实现和另一组使用他们的对象分离,实现抽象与实现分离。
2. 问题:一个抽象类的派生类必须使用多个实现,但是不能出现类数量的爆炸性增长,这一点和策略模式类似。
3. 解决方案:为所有实现类定义一个接口,供抽象类的所有派生类使用。
4. 参与者和协作者:Abstraction为要实现的对象(抽象)定义接口。Implementor为具体的实现类定义接口。Abstraction的派生类使用Implementor的派生类却无需知道自己使用哪一个ConcreteImplementor。
5. 效果:具体实现与调用该实现的对象解耦,提供可扩展性,客户对象无需操心具体实现问题。
6. 实现:将实现封装在一个抽象类中,在要实现的抽象基类中包含一个实现的引用。代码请参考06. 设计模式应用案例(上)。UML图如下:
五.模式总结和比较
1. Façade模式和Adapter模式比较
a. 类似之处:他们都是封装已有类的部分功能,以便于在新系统中使用。甚至类结构都是相似的:被封装对象作为一个成员出现在新对象中,当新对象接受到消息时,他将交付给被包装对象处理。
b. 区别之处:Façade模式不需按照某一个接口进行设计,无需用到多态,只需要更简单的接口;而Adapter模式需要按照某一个接口进行设计,需要多态,无需更简单的接口。总之:Façade模式简化了接口,而Adapter模式则将一个已有的接口转换成另一个。
2. Strategy模式与Bridge模式的比较
a. 相似之处:都存在一个对象使用聚合的方式引用另一个对象的抽象接口的情况,而且该抽象接口的实现可以有多种比且可以替换;两者在表象上都是调用者与被调用者之间的接偶,以及抽象接口与实现的分离,都是对变化的封装。
b. 区别之处:1)形式上的区别:在Bridge模式中不仅Implementor具有变化,而且Abstraction也可能发生变化,而且两者的变化是完全独立的,RefinedAbstraction与ConcreateImplementior之间松散耦合,它们仅仅通过Abstraction与Implementor之间的关系联系起来。而在Strategy模式中,并不考虑Context的变化,只有算法的可替代性;2)语意上的区别:Bridge模式强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作。Strategy模式强调Strategy抽象接口的提供的是一种算法,而Context则简单调用这些算法完成其操作。3)粒度上的区别:Bridge模式中不仅定义Implementor的接口而且定义Abstraction的接口,Abstraction的接口不仅仅是为了与Implementor通信而存在的,这也反映了结构型模式的特点:通过继承、聚合的方式组合类和对象以形成更大的结构;在Strategy模式中,Strategy和Context的接口都是两者之间的协作接口,并不涉及到其它的功能接口。
c. 总结:相对Strategy模式,Bridge模式要表达的内容要更多,结构也更加复杂。Bridge模式表达的主要意义其实是接口隔离的原则,即把本质上并不内聚的两种体系区别开来,使得它们可以松散的组合,而Strategy在解耦上还仅仅是某一个算法的层次,没有到体系这一层次。从结构图中可以看到,策略的结构是包容在Bridge结构中的,Bridge中必然存在着Strategy模式,Abstraction与Implementor之间就可以认为是Strategy模式,但是Bridge模式一般Implementor将提供一系列的成体系的操作,而且Implementor是具有状态和数据的静态结构。而且Bridge模式Abstraction也可以独立变化。