2012年9月28日星期五

桥接模式结构型模式(2)

桥接模式结构型模式(2)

前言

 回顾上一篇对适配器模式的介绍,其主要用于对现有对象的接口的适配封装,使其符合复用环境的接口要求,同时相对于类适配器来说,在java语言层面更适合使用对象组合的方式来实现适配器模式(主要是因为java或者.net语言不支持多继承机制),降低系统的耦合度,增加代码的灵活性和可维护性。其实,Favor Composition Over Inheritance原则在多个结构型模式中都有很明显的体现,接下来我们将要讲述的桥接模式便是一例。从复杂度来说,桥接模式算是23种设计模式中较为难懂和抽象的一种模式,认识和理解其本质,对我们设计人员来说将是一个很好的提高途径,因为它涉及了很多的面向对象设计中的原则,比如“开闭原则”及“组合/聚合复用原则”等,掌握了这些核心的设计原则,势必会对我们形成正确的设计思路和培养良好的设计风格有所帮助。

动机

在日常的系统开发中,某些对象类型的业务逻辑的复杂性,其内部具有两个或者多个维度的变化。如何应对这种多维度的变化?如何通过面向对象的方式来封装隔离多个维度的变化,同时又不增加额外的类设计的复杂程度呢?桥接模式给我们提供了一个良好的解决之道,接下来就让我们来深入地学习理解这个神奇的设计模式吧!

意图

将抽象部分与它的实现相分离,使它们都可以独立地变化

《设计模式》一书将桥接模式的意图概括地太过于精炼和抽象,对我们初学者来说,并不太好理解。在这里,我们就多来认识一下相关概念。所谓桥接,通俗地说是就将将不同的东西搭一个桥,将两者连通起来,这样彼此就可以相互通讯和使用呢。而在桥接模式中,我们是通过将抽象部分与实现部分间进行“搭桥“,这样两者就可以相互通信,发送信息呢。不过,在桥接模式中,桥接是单向的,也就是说只有抽象部分会使用具体的实现部分对象,也就是调用其中相应的实现方法,反之不成立,实现部分是不会也不应该调用抽象部分中的相应方法的,所以这个桥接只是一个单向桥接。

之所以需要桥接,是为了让彼此独立变化的抽象部分和实现部分之间进行联通, 这样虽然从程序结构上分开呢,但是通过这个”桥“,抽象部分就可以顺畅地调用到实现部分的功能呢。实现桥接,在实现上很简单,不是让抽象部分拥有实现部分的接口对象,然后抽象部分需要的时候可以通过这个接口对象来调用具体相应的功能呢。

最后,根据桥接模式的意图,是为了实现抽象与实现可以独立变化,都可以相互扩充。两者是相当松散的关系,它们之间是完全独立和分开的,唯一关联就是抽象部分会保留一个对实现部分的对外接口对象在需要的时候方便调用其相应的功能罢呢。

需要注意的是,我们这里说的抽象部分与实现部分是两个变化维度的抽象接口,也就是说它们之间不是我们平常所说的抽象与实现的关系,它们都可以单独地去变化(拥有不同的实现),通过对象组合的方式来连接抽象部分和实现部分,这一点值得大家理解清楚哦。

结构图

image

  1. 抽象化(Abstraction)角色:抽象类的接口,并保存一个对实现化对象的引用。
  2. 修正抽象化(Refined Abstraction)角色:扩充了Abstraction定义的接口,加强或者修正了父类对抽象化的定义。
  3. 实现化(Implementor)角色:定义实现类的接口,该接口不一定要与Abstraction的接口一致,事实上这两个接口可以完全不同。一般来说,Implementor接口仅定义提供了底层的基本操作,而Abstraction则定义了基于这些基本操作的较高层次的操作,理解这点很关键哦!总结一点就是,抽象化与实现化角色之间并不存在继承与实现的关系,两者之间只是存在一种委托的关系而已。
  4. 具体实现化(ConcreteImplementor)角色:实现了所有实现化角色所定义的接口。

代码示例

 1:  public abstract class Implementor{
 2:      public abstract void OperationImp();
 3:  }
 4:   
 5:  public class ConcreteImplementorA extends Implementor{
 6:      public void OperationImp(){
 7:          //省略实现...
 8:      }
 9:  }
10:   
11:  public class ConcreteImplementorB extends Implementor{
12:      public void OperationImp(){
13:          //省略实现...
14:      }
15:  }
16:  public abstract class Abstraction{
17:      protected Implementor implementor;
18:      public abstract void Operation();
19:  }
20:   
21:  public class RefinedAbstraction extends Abstraction{
22:      public RefinedAbstraction(Implementor implementor){
23:          this.implementor=implementor;
24:      }
25:   
26:      public void Operation(){
27:          //...
28:          implementor.OperationImp();//调用实现化角色实现的操作
29:          //...
30:      }
31:  }
32:   
33:  public class Client{
 
34:      public static void main(String[] args){
35:          Abstraction abstraction=new RefinedAbstraction(new ConcreteImplementorA());
36:          abstraction.Operation();
37:      }
38:  }

从上述示例代码中,我们应该可以进一步理解上文中对抽象化与实现化两者间的关系。其实,两者就是组合/聚合的关系,抽象化需要保存对实现化角色的引用,因为抽象化的接口实现过程中需要调用实现化提供的相应底层操作接口。相对于示例代码来说,类Abstraction就是抽象化角色,而类Implementor就是实现化角色,两者定义的接口规范并不相同,也没必要相同,因为两者所定义的接口在实现层面上就不同,从逻辑上来说,抽象化角色定义的接口应该是更高层次的接口,而实现化角色定义的接口应该是较低层次的接口。其实,我们也可以这样理解,实现化就是我们开头所说的变化维度,现在我们通过对象组合的方式来隔离当前维度的变化,使代码更具灵活性和可扩展性。

现实场景

考虑这样一个场景——信息发送。首先信息本身就有不同的种类,比如有普通信息和加急信息(即需要特殊处理的信息,比如需要对方回执或者说在原信息内容的开头加上特定信息),但是对它们都有一个共同的接口,就是发送信息的操作,这是一个变化的维度;另外,在现实的世界中,发送信息的手段有多种,比如通过短信(SMS)或者邮件(Email)的方式,这也一个变化的维度。现在,我们有两个不同的变化维度,一个是信息种类,一个是信息发送手段,如果我们通过传统的继承方式,那么设计出来的UML图大概会是下面的样子:

image

虽然从上图来说,现在好像结构并不是很复杂,而且也很合理。但是大家有没想过,如果此时再添加一种信息种类的类,通过继承的方式来扩展的话,这个时候我们需要添加三个类,一个是新类型的信息类,另外两个是针对这种信息类型的两种不同发送手段即短信和邮件的方式。如果需求至此为止的话,这样的结构设计还是可以勉强应付的,但是不幸的是,需求是不断变化的,或许在将来的某个时间里,我们又需要添加一种全新的信息发送手段,比如说现在用的很火的微信方式,那么这个时候需要添加的新类的数目就会很多呢。大家应该可以想像出此时的类结构图会是什么样子,我是不想再画出那样”复杂“的类结构图来展示给大家看呢,也没必要,继承关系太多,难于维护,最致命的一点是这样的继承方式扩展性太差。其实这里不光是类数目的激增问题,更要命的是,这样的设计已经全然违反了面向对象设计的类的单一职责原则上呢,也就是一个类应该只有一个引进它变化的原因,而这里,我们发现Message类却存在两个变化点,一个是信息种类,一个是信息发送手段。这样的设计无疑是脆弱的,不合理的,接下我们通过桥接模式来重新设计以上场景,对比一下,两者的区别之处:

image

上图是使用桥接模式针对信息发送场景设计的类结构图,我们不再通过继承的方式来耦合多维度的变化,而是通过对象组合的方式来降低它们之间的耦合度,让信息种类与信息发送手段两个维度可以自由扩展,而将它们连接在一起的方法便是通过”桥“的方式,使信息种类可以通过信息发送手段对象来调用到相应的发送方法。通过桥接的方式,类的结构图变得十分地简洁、清晰,而且拥有良好的可扩展性:和上文描述的一样,不管此时增加新的信息种类还是增加新的信息发送手段,我们都无需对原有类进行修改,只需增加必须的新类即可,甚至都无需考虑新添加的类与原来类间的关系,我们只需要继承对应的父类即可,它们已经帮我们打理好了两个变化维度之间的连通方式。下面给出上述场景示意性的代码片段吧,与我们上文的示例代码结构上几乎完全一致!

 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:  }

通过示例代码,我们可以清楚地看到,将信息种类和信息发送手段联系起来的方式就是通过对象组合的方式来完成,在信息种类的父类中保存一个对信息发送手段接口的引用,以便在需要时调用其相应的实现。好呢,通过这个简单的场景相信大家也对桥接模式的应用有了较深刻的理解呢,对其举例就在此打住吧,大家可以充分联想各种适用于桥接模式的应用场景,深入思考,相信会对桥接模式的本质有一个更全面准确的理解。

实现要点

  1. 桥接模式使用”对象组合“的方式来解耦抽象与实现之间绑定关系,使得各自可以沿着各自的维度来扩展和变化。
  2. 抽象部分与实现部分沿着各自维度的变化指的就是实现它们对应的子类,也就是不同的实现,这样将两者结合的效果就可以得到多种种类(比如信息种类)的不同实现(信息发送手段)。
  3. 桥接模式之所以不使用多继承方式,是因为继承方案容易违背类的单一职责原则,复用性和可扩展性都不及对象组合方式。
  4. 桥接模式是为应对两个维度的各自变化扩展问题,如果这两个变化的维度(或者某个维度)的变化程度并不十分剧烈,使用多继承的方式也未尝不可。

运用效果

  1. 分离抽象和实现部分:桥接模式分离了抽象部分和实现部分,让抽象部分和实现部分独立开来,分别定义接口,有助于对系统进行分层,产生更好的结构化的系统。
  2. 良好的扩展性:桥接模式把抽象部分和实现部分分离开来,而且分别定义了接口,这样两者就可以分别独立扩展,并互不影响,极大地提高系统的扩展性。
  3. 可动态地切换实现:由于桥接模式把抽象部分和实现部分分离开来,这样在实现的过程中,我们就可以实现动态的选择和使用具体的实现部分呢。因为实现部分不是固定的绑定在一个抽象接口上,可以实现运行期间动态地切换不同实现部分。
  4. 大大减小了子类的数目:针对两个维度的变化情况,如果采用继承的方式,需要的两个维度上的可变化数量(即具体子类数目)的乘积数目;而采用桥接模式,需要的只是两个维度上的可变数量(即具体子类数目)的和个数。极大地减小子类的数目。

适用性

  1. 不希望在抽象和实现部分之间有一个固定的绑定关系时,也就是可以在运行时刻可以动态地切换实现部分的不同具体实现情况。
  2. 类的抽象和它的实现都应该可以通过生成子类的方法加以扩充。 也就说可以对不同的抽象接口与实现部分进行组合,并分别对它们时行扩充。
  3. 设计要求实现化角色的任何改变都不应当影响客户端,也就是实现化角色的改变对客户端是透明的。
  4. 组件有多于一个抽象化角色和实现化角色,系统需要它们之间动态解耦时。

相关模式

  1. 桥接模式与策略模式:两者之间的类结构图比较相似,可以将策略模式的Context当作是使用实现化接口的对象(即抽象化角色),这样Strategy就是某一个具体的实现化部分呢。从这点来说,使用桥接模式可以模拟实现策略模式的功能,但是是有一点需要注意的是,策略模式的Context不能扩展,而桥接模式的抽象化角色却可以自由扩展。再者就是两者的目的不一样,策略模式是封装一系列算法,使得这些算法可以相互替换; 而桥接模式的目的是分离抽象部分和实现部分,使得它们可以独立地变化。
  2. 桥接模式与状态模式:应该说两者的结构上来说,与状态模式与策略模式是一样的,两者的关系也基本上类似于桥接模式与策略模式的关系。不同的还是各自的目的,状态模式是封装不同状态下对应的行为,在内部状态改变的时候改变对象的行为。
  3. 桥接模式与抽象工厂模式:其实凡是需要创建对象的地方就需要创建型模式,而使用最多的创建型模式就是工厂方法模式和抽象工厂模式。在桥接模式的实现过程中,抽象化部分需要引用一个实现部分的具体实现对象,而这个实现对象的创建工作可以交由工厂方法或者抽象工厂模式来完成,甚至如果所需的实现化角色的种类不多,完全可以使用简单工厂方法来胜任。
  4. 桥接模式与适配器模式:适配器模式主要用于解决原本由于接口不兼容而不能一起工作的那些类,使得它们可以一起工作,而桥接模式重点在于分离抽象部分和实现部分,使它们彼此可以沿着各自的维度自由扩展、变化。在使用时间上,适配器模式一般用于系统设计实现之后,而桥接模式一般用于系统设计实现之时。

总结

桥接模式的本质是:分离抽象和实现。只有将两者分离,它们才能独立地变化,也只有两者可以相对独立地变化时,系统才会有更好的可扩展性和可维护性。桥接模式很好地遵循了开闭原则,也较好地体现了Favor Composition Over Inheritance(优先使用对象组合/聚合原则)。大家是否也已经体会到呢?原因上文已经介绍地很明白呢。客观地说,桥接模式比较难理解,虽然意图只是简单的一句话,但是里面包含的东西却很深刻,需要我们大家反复领悟思考,才会做到真正的融会贯通。对桥接模式的介绍就到此为止吧,接下来,我们将继续介绍下一种结构型设计模式——组合模式,敬请期待!

 

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章

TAG: