回顾上一篇对适配器模式的介绍,其主要用于对现有对象的接口的适配封装,使其符合复用环境的接口要求,同时相对于类适配器来说,在java语言层面更适合使用对象组合的方式来实现适配器模式(主要是因为java或者.net语言不支持多继承机制),降低系统的耦合度,增加代码的灵活性和可维护性。其实,Favor Composition Over Inheritance原则在多个结构型模式中都有很明显的体现,接下来我们将要讲述的桥接模式便是一例。从复杂度来说,桥接模式算是23种设计模式中较为难懂和抽象的一种模式,认识和理解其本质,对我们设计人员来说将是一个很好的提高途径,因为它涉及了很多的面向对象设计中的原则,比如“开闭原则”及“组合/聚合复用原则”等,掌握了这些核心的设计原则,势必会对我们形成正确的设计思路和培养良好的设计风格有所帮助。
在日常的系统开发中,某些对象类型的业务逻辑的复杂性,其内部具有两个或者多个维度的变化。如何应对这种多维度的变化?如何通过面向对象的方式来封装隔离多个维度的变化,同时又不增加额外的类设计的复杂程度呢?桥接模式给我们提供了一个良好的解决之道,接下来就让我们来深入地学习理解这个神奇的设计模式吧!
将抽象部分与它的实现相分离,使它们都可以独立地变化。
《设计模式》一书将桥接模式的意图概括地太过于精炼和抽象,对我们初学者来说,并不太好理解。在这里,我们就多来认识一下相关概念。所谓桥接,通俗地说是就将将不同的东西搭一个桥,将两者连通起来,这样彼此就可以相互通讯和使用呢。而在桥接模式中,我们是通过将抽象部分与实现部分间进行“搭桥“,这样两者就可以相互通信,发送信息呢。不过,在桥接模式中,桥接是单向的,也就是说只有抽象部分会使用具体的实现部分对象,也就是调用其中相应的实现方法,反之不成立,实现部分是不会也不应该调用抽象部分中的相应方法的,所以这个桥接只是一个单向桥接。
之所以需要桥接,是为了让彼此独立变化的抽象部分和实现部分之间进行联通, 这样虽然从程序结构上分开呢,但是通过这个”桥“,抽象部分就可以顺畅地调用到实现部分的功能呢。实现桥接,在实现上很简单,不是让抽象部分拥有实现部分的接口对象,然后抽象部分需要的时候可以通过这个接口对象来调用具体相应的功能呢。
最后,根据桥接模式的意图,是为了实现抽象与实现可以独立变化,都可以相互扩充。两者是相当松散的关系,它们之间是完全独立和分开的,唯一关联就是抽象部分会保留一个对实现部分的对外接口对象在需要的时候方便调用其相应的功能罢呢。
需要注意的是,我们这里说的抽象部分与实现部分是两个变化维度的抽象接口,也就是说它们之间不是我们平常所说的抽象与实现的关系,它们都可以单独地去变化(拥有不同的实现),通过对象组合的方式来连接抽象部分和实现部分,这一点值得大家理解清楚哦。
抽象化(Abstraction)角色:抽象类的接口,并保存一个对实现化对象的引用。 修正抽象化(Refined Abstraction)角色:扩充了Abstraction定义的接口,加强或者修正了父类对抽象化的定义。 实现化(Implementor)角色:定义实现类的接口,该接口不一定要与Abstraction的接口一致,事实上这两个接口可以完全不同。一般来说,Implementor接口仅定义提供了底层的基本操作,而Abstraction则定义了基于这些基本操作的较高层次的操作,理解这点很关键哦!总结一点就是,抽象化与实现化角色之间并不存在继承与实现的关系,两者之间只是存在一种委托的关系而已。 具体实现化(ConcreteImplementor)角色:实现了所有实现化角色所定义的接口。
从上述示例代码中,我们应该可以进一步理解上文中对抽象化与实现化两者间的关系。其实,两者就是组合/聚合的关系,抽象化需要保存对实现化角色的引用,因为抽象化的接口实现过程中需要调用实现化提供的相应底层操作接口。相对于示例代码来说,类Abstraction就是抽象化角色,而类Implementor就是实现化角色,两者定义的接口规范并不相同,也没必要相同,因为两者所定义的接口在实现层面上就不同,从逻辑上来说,抽象化角色定义的接口应该是更高层次的接口,而实现化角色定义的接口应该是较低层次的接口。其实,我们也可以这样理解,实现化就是我们开头所说的变化维度,现在我们通过对象组合的方式来隔离当前维度的变化,使代码更具灵活性和可扩展性。
考虑这样一个场景——信息发送。首先信息本身就有不同的种类,比如有普通信息和加急信息(即需要特殊处理的信息,比如需要对方回执或者说在原信息内容的开头加上特定信息),但是对它们都有一个共同的接口,就是发送信息的操作,这是一个变化的维度;另外,在现实的世界中,发送信息的手段有多种,比如通过短信(SMS)或者邮件(Email)的方式,这也一个变化的维度。现在,我们有两个不同的变化维度,一个是信息种类,一个是信息发送手段,如果我们通过传统的继承方式,那么设计出来的UML图大概会是下面的样子:
虽然从上图来说,现在好像结构并不是很复杂,而且也很合理。但是大家有没想过,如果此时再添加一种信息种类的类,通过继承的方式来扩展的话,这个时候我们需要添加三个类,一个是新类型的信息类,另外两个是针对这种信息类型的两种不同发送手段即短信和邮件的方式。如果需求至此为止的话,这样的结构设计还是可以勉强应付的,但是不幸的是,需求是不断变化的,或许在将来的某个时间里,我们又需要添加一种全新的信息发送手段,比如说现在用的很火的微信方式,那么这个时候需要添加的新类的数目就会很多呢。大家应该可以想像出此时的类结构图会是什么样子,我是不想再画出那样”复杂“的类结构图来展示给大家看呢,也没必要,继承关系太多,难于维护,最致命的一点是这样的继承方式扩展性太差。其实这里不光是类数目的激增问题,更要命的是,这样的设计已经全然违反了面向对象设计的类的单一职责原则上呢,也就是一个类应该只有一个引进它变化的原因,而这里,我们发现Message类却存在两个变化点,一个是信息种类,一个是信息发送手段。这样的设计无疑是脆弱的,不合理的,接下我们通过桥接模式来重新设计以上场景,对比一下,两者的区别之处:
上图是使用桥接模式针对信息发送场景设计的类结构图,我们不再通过继承的方式来耦合多维度的变化,而是通过对象组合的方式来降低它们之间的耦合度,让信息种类与信息发送手段两个维度可以自由扩展,而将它们连接在一起的方法便是通过”桥“的方式,使信息种类可以通过信息发送手段对象来调用到相应的发送方法。通过桥接的方式,类的结构图变得十分地简洁、清晰,而且拥有良好的可扩展性:和上文描述的一样,不管此时增加新的信息种类还是增加新的信息发送手段,我们都无需对原有类进行修改,只需增加必须的新类即可,甚至都无需考虑新添加的类与原来类间的关系,我们只需要继承对应的父类即可,它们已经帮我们打理好了两个变化维度之间的连通方式。下面给出上述场景示意性的代码片段吧,与我们上文的示例代码结构上几乎完全一致!
1: public abstract class MessageImplementor{ 2: public abstract void Send(); 3: } 4: 5: public class MessageSMS extends MessageImplementor{ 6: public void Send(){ 7: //短信发送信息的具体实现... 8: } 9: } 10: 11: public class MessageEmail extends MessageImplementor{ 12: public void Send(){ 13: //邮件发送信息的具体实现... 14: } 15: } 16: public abstract class Message{ 17: protected MessageImplementor implementor; 18: public abstract void SendMessage(); 19: } 20: 21: public class CommonMessage extends Message{ 22: public CommonMessage(MessageImplementor implementor){ 23: this.implementor=implementor; 24: } 25: 26: public void SendMessage(){ 27: //... 28: implementor.Send();//调用具体的短信发送实现操作 29: //... 30: } 31: } 32: 33: public class UrgencyMessage extends Message{ 34: public UrgencyMessage(MessageImplementor implementor){ 35: this.implementor=implementor; 36: } 37: 38: public void SendMessage(){ 39: //... 40: implementor.Send();//调用具体的短信发送实现操作 41: //... 42: } 43: } 44: 45: public class Client{ 46: public static void main(String[] args){ 47: Message message=new CommonMessage(new MessageSMS()); 48: message.SendMessage(); 49: } 50: }通过示例代码,我们可以清楚地看到,将信息种类和信息发送手段联系起来的方式就是通过对象组合的方式来完成,在信息种类的父类中保存一个对信息发送手段接口的引用,以便在需要时调用其相应的实现。好呢,通过这个简单的场景相信大家也对桥接模式的应用有了较深刻的理解呢,对其举例就在此打住吧,大家可以充分联想各种适用于桥接模式的应用场景,深入思考,相信会对桥接模式的本质有一个更全面准确的理解。
桥接模式的本质是:分离抽象和实现。只有将两者分离,它们才能独立地变化,也只有两者可以相对独立地变化时,系统才会有更好的可扩展性和可维护性。桥接模式很好地遵循了开闭原则,也较好地体现了Favor Composition Over Inheritance(优先使用对象组合/聚合原则)。大家是否也已经体会到呢?原因上文已经介绍地很明白呢。客观地说,桥接模式比较难理解,虽然意图只是简单的一句话,但是里面包含的东西却很深刻,需要我们大家反复领悟思考,才会做到真正的融会贯通。对桥接模式的介绍就到此为止吧,接下来,我们将继续介绍下一种结构型设计模式——组合模式,敬请期待!
参考资料:
程杰著《大话设计模式》一书 陈臣等著《研磨设计模式》一书 GOF著《设计模式》一书 Terrylee .Net设计模式系列文章 吕震宇老师 设计模式系列文章转载于:https://www.cnblogs.com/JackyTecblog/archive/2012/09/28/2707345.html
