研磨设计模式之 桥接模式-4
3.4 广义桥接-Java中无处不桥接
使用Java编写程序,一个很重要的原则就是“面向接口编程”,说得准确点应该是“面向抽象编程”,由于在Java开发中,更多的使用接口而非抽象类,因此通常就说成“面向接口编程”了。
接口把具体的实现和使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。使用接口的程序结构如图12所示:
图12 使用接口的程序结构示意图
可能有些朋友会觉得,听起来怎么像是桥接模式的功能呢?没错,如果把桥接模式的抽象部分先稍稍简化一下,暂时不要RefinedAbstraction部分,那么就跟上面的结构图差不多了。去掉RefinedAbstraction后的简化的桥接模式结构示意图如图13所示:
图13 简化的桥接模式结构示意图
是不是差不多呢?有朋友可能会觉得还是有很大差异,差异主要在:前面接口的客户程序是直接使用接口对象,不像桥接模式的抽象部分那样,是持有具体实现部分的接口,这就导致画出来的结构图,一个是依赖,一个是聚合关联。
请思考它们的本质功能,桥接模式中的抽象部分持有具体实现部分的接口,最终目的是什么,还不是需要通过调用具体实现部分的接口中的方法,来完成一定的功能,这跟直接使用接口没有什么不同,只是表现形式有点不一样。再说,前面那个使用接口的客户程序也可以持有相应的接口对象,这样从形式上就一样了。
也就是说,从某个角度来讲,桥接模式不过就是对“面向抽象编程”这个设计原则的扩展。正是通过具体实现的接口,把抽象部分和具体的实现分离开来,抽象部分相当于是使用实现部分接口的客户程序,这样抽象部分和实现部分就松散耦合了,从而可以实现相互独立的变化。
这样一来,几乎可以把所有面向抽象编写的程序,都视作是桥接模式的体现,至少算是简化的桥接模式,就算是广义的桥接吧。而Java编程很强调“面向抽象编程”,因此,广义的桥接,在Java中可以说是无处不在。
再举个大家最熟悉的例子来示例一下。在Java应用开发中,分层实现算是最基本的设计方式了吧,就拿大家最熟的三层架构来说,表现层、逻辑层和数据层,或许有些朋友对它们称呼的名称不同,但都是同一回事情。
三层的基本关系是表现层调用逻辑层,逻辑层调用数据层,通过什么来进行调用呢?当然是接口了,它们的基本结构如图14所示:
图14 基本的三层架构示意图
通过接口来进行调用,使得表现层和逻辑层分离开来,也就是说表现层的变化,不会影响到逻辑层,同理逻辑层的变化不会影响到表现层。这也是同一套逻辑层和数据层,就能够同时支持不同的表现层实现的原因,比如支持Swing或Web方式的表现层。
在逻辑层和数据层之间也是通过接口来调用,同样使得逻辑层和数据层分离开,使得它们可以独立的扩展。尤其是数据层,可能会有很多的实现方式,比如:数据库实现、文件实现等,就算是数据库实现,又有针对不同数据库的实现,如Oracle、DB2等等。
总之,通过面向抽象编程,三层架构的各层都能够独立的扩展和变化,而不会对其它层次产生影响。这正好是桥接模式的功能,实现抽象和实现的分离,从而使得它们可以独立的变化。当然三层架构不只是在一个地方使用桥接模式,而是至少在两个地方来使用了桥接模式,一个在表现层和逻辑层之间,一个在逻辑层和数据层之间。
下面先分别看看这两个使用桥接模式的地方的程序结构,然后再综合起来看看整体的程序结构。先看看逻辑层和数据层之间的程序结构,如图15所示:
图15 逻辑层和数据层的程序结构示意图
再看看表现层和逻辑层之间的结构示意,如图16所示:
图16 表现层和逻辑层的结构示意图
然后再把它们结合起来,看看结合后的程序结构,如图17所示:
图17 三层结合的结构示意图
从广义桥接模式的角度来看,平日熟悉的三层架构其实就是在组合使用桥接模式。从这个图还可以看出,桥接模式是可以连续组合使用的,一个桥接模式的实现部分,可以作为下一个桥接模式的抽象部分。如此类推,可以从三层架构扩展到四层、五层、直到N层架构,都可以使用桥接模式来组合。
如果从更本质的角度来看,基本上只要是面向抽象编写的Java程序,都可以视为是桥接模式的应用,都是让抽象和实现相分离,从而使它们能独立的变化。不过只要分离的目的达到了,叫不叫桥接模式就无所谓了。
3.5 桥接模式的优缺点
- 分离抽象和实现部分
桥接模式分离了抽象和实现部分,从而极大地提高了系统的灵活性。让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统。对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了。 - 更好的扩展性
由于桥接模式把抽象和实现部分分离开了,而且分别定义接口,这就使得抽象部分和实现部分可以分别独立的扩展,而不会相互影响,从而大大的提高了系统的可扩展性。 - 可动态切换实现
由于桥接模式把抽象和实现部分分离开了,那么在实现桥接的时候,就可以实现动态的选择和使用具体的实现,也就是说一个实现不再是固定的绑定在一个抽象接口上了,可以实现运行期间动态的切换实现。 - 可减少子类的个数
根据前面的讲述,对于有两个变化纬度的情况,如果采用继承的实现方式,大约需要两个纬度上的可变化数量的乘积个子类;而采用桥接模式来实现的话,大约需要两个纬度上的可变化数量的和个子类。可以明显地减少子类的个数。
3.6 思考桥接模式
1:桥接模式的本质
桥接模式的本质:分离抽象和实现。
桥接模式最重要的工作就是分离抽象部分和实现部分,这是解决问题的关键。只有把抽象和实现分离开了,才能够让它们可以独立的变化;只有抽象和实现可以独立的变化,系统才会有更好的可扩展性、可维护性。
至于其它的好处,比如:可以动态切换实现、可以减少子类个数等。都是把抽象部分和实现部分分离过后,带来的,如果不把抽象部分和实现部分分离开,那就一切免谈了。所以综合来说,桥接模式的本质在于“分离抽象和实现”。
2:对设计原则的体现
(1)桥接模式很好的实现了开闭原则。
通常应用桥接模式的地方,抽象部分和实现部分都是可变化的,也就是应用会有两个变化纬度,桥接模式就是找到这两个变化,并分别封装起来,从而合理的实现OCP。
在使用桥接模式的时候,通常情况下,顶层的Abstraction和Implementor是不变的,而具体的Implementor的实现,是可变的,由于Abstraction是通过接口来操作具体的实现,因此具体的Implementor的实现是可以扩展的,根据需要可以有多个具体的实现。
同样的,RefinedAbstraction也是可变的,它继承并扩展Abstraction,通常在RefinedAbstraction的实现里面,会调用Abstraction中的方法,通过组合使用来完成更多的功能,这些功能常常是与具体业务有关系的功能。
桥接模式还很好的体现了:多用对象组合,少用对象继承。
在前面的示例中,如果使用对象继承来扩展功能,不但让对象之间有很强的耦合性,而且会需要很多的子类才能完成相应的功能,前面已经讲述过了,需要两个纬度上的可变化数量的乘积个子类。
而采用对象的组合,松散了对象之间的耦合性,不但使每个对象变得简单和可维护,还大大减少了子类的个数,根据前面的讲述,大约需要两个纬度上的可变化数量的和这么多个子类。
3:何时选用桥接模式
建议在如下情况中,选用桥接模式:
- 如果你不希望在抽象和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象和实现部分分开,然后在程序运行期间来动态的设置抽象部分需要用到的具体的实现,还可以动态切换具体的实现。
- 如果出现抽象部分和实现部分都应该可以扩展的情况,可以采用桥接模式,让抽象部分和实现部分可以独立的变化,从而可以灵活的进行单独扩展,而不是搅在一起,扩展一边会影响到另一边。
- 如果希望实现部分的修改,不会对客户产生影响,可以采用桥接模式,客户是面向抽象的接口在运行,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,也可以说对客户是透明的。
- 如果采用继承的实现方案,会导致产生很多子类,对于这种情况,可以考虑采用桥接模式,分析功能变化的原因,看看是否能分离成不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目。
3.7 相关模式
- 桥接模式和策略模式
这两个模式有很大的相似之处。
如果把桥接模式的抽象部分简化来看,如果暂时不去扩展Abstraction,也就是去掉RefinedAbstraction。桥接模式简化过后的结构图参见图13。再看策略模式的结构图参见图17.1。会发现,这个时候它们的结构都类似如图18所示:
图18 桥接模式和策略模式结构示意图
通过上面的结构图,可以体会到桥接模式和策略模式是如此相似。可以把策略模式的Context视做是使用接口的对象,而Strategy就是某个接口了,具体的策略实现那就相当于接口的具体实现。这样看来的话,某些情况下,可以使用桥接模式来模拟实现策略模式的功能。
这两个模式虽然相似,也还是有区别的。最主要的是模式的目的不一样,策略模式的目的是封装一系列的算法,使得这些算法可以相互替换;而桥接模式的目的是分离抽象和实现部分,使得它们可以独立的变化。 - 桥接模式和状态模式
由于从模式结构上看,状态模式和策略模式是一样的,这两个模式的关系也基本上类似于桥接模式和策略模式的关系。
只不过状态模式的目的是封装状态对应的行为,并在内部状态改变的时候改变对象的行为。 - 桥接模式和模板方法模式
这两个模式有相似之处。
虽然标准的模板方法模式是采用继承来实现的,但是模板方法也可以通过回调接口的方式来实现,如果把接口的实现独立出去,那就类似于模板方法通过接口去调用具体的实现方法了。这样的结构就和简化的桥接模式类似了。
可以使用桥接模式来模拟实现模板方法模式的功能。如果在实现Abstraction对象的时候,在里面定义方法,方法里面就是某个固定的算法骨架,也就是说这个方法就相当于模板方法。在模板方法模式里,是把不能确定实现的步骤延迟到子类去实现;现在在桥接模式里面,把不能确定实现的步骤委托给具体实现部分去完成,通过回调实现部分的接口,来完成算法骨架中的某些步骤。这样一来,就可以实现使用桥接模式来模拟实现模板方法模式的功能了。
使用桥接模式来模拟实现模板方法模式的功能,还有个潜在的好处,就是模板方法也可以很方便的扩展和变化了。在标准的模板方法里面,一个问题就是当模板发生变化的时候,所有的子类都要变化,非常不方便。而使用桥接模式来实现类似的功能,就没有这个问题了。
另外,这里只是说从实现具体的业务功能上,桥接模式可以模拟实现出模板方法模式能实现的功能,并不是说桥接模式和模板方法模式就变成一样的,或者是桥接模式就可以替换掉模板方法模式了。要注意它们本身的功能、目的、本质思想都是不一样的。 - 桥接模式和抽象工厂模式
这两个模式可以组合使用。
桥接模式中,抽象部分需要获取相应的实现部分的接口对象,那么谁来创建实现部分的具体实现对象呢?这就是抽象工厂模式派上用场的地方。也就是使用抽象工厂模式来创建和配置一个特定的具体实现的对象。
事实上,抽象工厂主要是用来创建一系列对象的,如果创建的对象很少,或者是很简单,还可以采用简单工厂,可以达到一样的效果,但是会比抽象工厂来得简单。 - 桥接模式和适配器模式
这两个模式可以组合使用。
这两个模式功能是完全不一样的,适配器模式的功能主要是用来帮助无关的类协同工作,重点在解决原本由于接口不兼容而不能一起工作的那些类,使得它们可以一起工作。而桥接模式则重点在分离抽象和实现部分。
所以在使用上,通常在系统设计完成过后,才会考虑使用适配器模式;而桥接模式,是在系统开始的时候就要考虑使用。
虽然功能上不一样,这两个模式还是可以组合使用的,比如:已有实现部分的接口,但是有些不太适应现在新的功能对接口的需要,完全抛弃吧,有些功能还用得上,该怎么办呢?那就使用适配器来进行适配,使得旧的接口能够适应新的功能的需要。
研磨设计模式结束,谢谢捧场!