First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 3187 评论 :: 358 引用

一、 桥梁(Bridge)模式

桥梁模式是一个非常有用的模式,也是比较复杂的一个模式。熟悉这个模式对于理解面向对象的设计原则,包括"开-闭"原则(OCP)以及组合/聚合复用原则(CARP)都很有帮助。理解好这两个原则,有助于形成正确的设计思想和培养良好的设计风格。

注:《Java与模式》一书认为Bridge模式不是一个使用频率很高的模式,我不太赞同,我认为Bridge模式中蕴涵了很多设计模式的关键思想在里面,所以我这里采纳了《Design Patterns Explained》一书的作者Alan Shalloway与James R. Trott的观点:The Bridge pattern is quite a bit more complex than the other patterns you just learned; it is also much more useful.

桥梁模式的用意

【GOF95】在提出桥梁模式的时候指出,桥梁模式的用意是"将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化"。这句话有三个关键词,也就是抽象化、实现化和脱耦。

抽象化

存在于多个实体中的共同的概念性联系,就是抽象化。作为一个过程,抽象化就是忽略一些信息,从而把不同的实体当做同样的实体对待【LISKOV94】。

实现化

抽象化给出的具体实现,就是实现化。

脱耦

所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。

将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成为弱关联。因此,桥梁模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥梁模式的用意。


二、 桥梁模式的结构

桥梁模式【GOF95】是对象的结构模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。

下图所示就是一个实现了桥梁模式的示意性系统的结构图。

可以看出,这个系统含有两个等级结构,也就是:

  • 由抽象化角色和修正抽象化角色组成的抽象化等级结构。
  • 由实现化角色和两个具体实现化角色所组成的实现化等级结构。

桥梁模式所涉及的角色有:

  • 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
  • 修正抽象化(Refined Abstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
  • 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
  • 具体实现化(Concrete Implementor)角色:这个角色给出实现化角色接口的具体实现。


三、 桥梁模式的示意性源代码

// Bridge pattern -- Structural example  
using System;

// "Abstraction"
class Abstraction
{
  
// Fields
  protected Implementor implementor;

  
// Properties
  public Implementor Implementor
  
{
    
set{ implementor = value; }
  }


  
// Methods
  virtual public void Operation()
  
{
    implementor.Operation();
  }

}


// "Implementor"
abstract class Implementor
{
  
// Methods
  abstract public void Operation();
}


// "RefinedAbstraction"
class RefinedAbstraction : Abstraction
{
  
// Methods
  override public void Operation()
  
{
    implementor.Operation();
  }

}


// "ConcreteImplementorA"
class ConcreteImplementorA : Implementor
{
  
// Methods
  override public void Operation()
  
{
    Console.WriteLine(
"ConcreteImplementorA Operation");
  }

}


// "ConcreteImplementorB"
class ConcreteImplementorB : Implementor
{
  
// Methods
  override public void Operation()
  
{
    Console.WriteLine(
"ConcreteImplementorB Operation");
  }

}


/// <summary>
/// Client test
/// </summary>

public class Client
{
  
public static void Main( string[] args )
  
{
    Abstraction abstraction 
= new RefinedAbstraction();

    
// Set implementation and call
    abstraction.Implementor = new ConcreteImplementorA();
    abstraction.Operation();

    
// Change implemention and call
    abstraction.Implementor = new ConcreteImplementorB();
    abstraction.Operation();
  }

}


四、 调制解调器问题

感觉《敏捷软件开发-原则、模式与实践》中关于Bridge模式的例子很好。(《Java与模式》一书33章的对变化的封装一节也写得很不错,推荐大家读一读。它深入的阐述了《Design Patterns Explained》一书中"1)Design to interfaces. 2)Favor composition over inheritance. 3)Find what varies and encapsulate it"的三个观点。)。

如图所示,有大量的调制解调器客户程序在使用Modem接口。Modem接口被几个派生类HayesModem、USRoboticsModem和EarniesModem实现。它很好地遵循了OCP、LSP和DIP。当增加新种类的调制解调器时,调制解调器的客户程序不会受影响。

假定这种情形持续了几年,并有许多调制解调器的客户程序都在使用着Modem接口。现出现了一种不拨号的调制解调器,被称为专用调制解调器。它们位于一条专用连接的两端。有几个新应用程序使用这些专用调制解调器,它们无需拨号。我们称这些使用者为DedUser。但是,客户希望当前所有的调制解调器客户程序都可以使用这些专用调制解调器。他们不希望去更改许许多多的调制解调器客户应用程序,所以完全可以让这些调制解调器客户程序去拨一些假(dummy)电话号码。

如果能选择的话,我们会把系统的设计更改为下图所示的那样。

我们把拨号和通信功能分离为两个不同的接口。原来的调制解调器实现这两个接口,而调制解调器客户程序使用这两个接口。DedUser只使用Modem接口,而DedicateModem只实现Modem接口。但这样做会要求我们更改所有的调制解调器客户程序--这是客户不允许的。

一个可能的解决方案是让DedicatedModem从Modem派生并且把dial方法和hangup方法实现为空,就像下面这样:

几个月后,已经有了大量的DedUser,此时客户提出了一个新的更改。为了能拨国际电话号码、信用卡电话、PIN标识电话等等,必修对现有dial中使用char[10]存储号码改为能够拨打任意长度的电话号码。

显然,所有的调制解调器客户程序都必须更改。客户同意了对调制解调器客户程序的更改,因为他们别无选择。糟糕的是,现在必须要去告诉DedUser的编写者,他们必须要更改他们的代码!你可以想象他们听到这个会有多高兴。本来他们是不用调用dial的。

这就是许多项目都会具有的那种有害的混乱依赖关系。系统某一部分中的一个杂凑体(kludge)创建了一个有害的依赖关系,最终导致系统中完全无关的部分出现问题。

如果使用ADAPTER模式解决最初的问题的话,就可以避免这个严重问题。如图:

请注意,杂凑体仍然存在。适配器仍然要模拟连接状态。然而,所有的依赖关系都是从适配器发起的。杂凑体和系统隔离,藏身于几乎无人知晓的适配器中。

BRIDGE模式

看待这个问题,还有另外一个方式。现在,出现了另外一种切分Modem层次结构的方式。如下图:

这不是一个理想的结构。每当增加一款新硬件时,就必须创建两个新类--一个针对专用的情况,一个针对拨号的情况。每当增加一种新连接类型时,就必须创建3个新类,分别对应3款不同的硬件。如果这两个自由度根本就是不稳定的,那么不用多久,就会出现大量的派生类。

在类型层次结构具有多个自由度的情况中,BRIDGE模式通常是有用的。我们可以把这些层次结构分开并通过桥把它们结合到一起,而不是把它们合并起来。如图:

我们把调制解调器类层次结构分成两个层次结构。一个表示连接方法,另一个表示硬件。

这个结构虽然复杂,但是很有趣。它的创建不会影响到调制解调器的使用者,并且还完全分离了连接策略和硬件实现。ModemConnectController的每个派生类代表了一个新的连接策略。在这个策略的实现中可以使用sendlmp、receivelmp、diallmp和hanglmp。新imp方法的增加不会影响到使用者。可以使用ISP来给连接控制类增加新的接口。这种做法可以创建出一条迁移路径,调制解调器的客户程序可以沿着这条路径慢慢地得到一个比dial和hangup层次更高的API。


五、 另外一个实际应用Bridge模式的例子

该例子演示了业务对象(BusinessObject)通过Bridge模式与数据对象(DataObject)解耦。数据对象的实现可以在不改变客户端代码的情况下动态进行更换。

// Bridge pattern -- Real World example
using System;
using System.Collections;

// "Abstraction"
class BusinessObject
{
  
// Fields
  private DataObject dataObject;
  
protected string group;

  
// Constructors
  public BusinessObject( string group )
  
{
    
this.group = group;
  }


  
// Properties
  public DataObject DataObject
  
{
    
set{ dataObject = value; }
    
getreturn dataObject; }
  }


  
// Methods
  virtual public void Next()
  
{ dataObject.NextRecord(); }

  
virtual public void Prior()
  
{ dataObject.PriorRecord(); }

  
virtual public void New( string name )
  
{ dataObject.NewRecord( name ); }

  
virtual public void Delete( string name )
  
{ dataObject.DeleteRecord( name ); }

  
virtual public void Show()
  
{ dataObject.ShowRecord(); }

  
virtual public void ShowAll()
  
{
    Console.WriteLine( 
"Customer Group: {0}", group );
    dataObject.ShowAllRecords();
  }

}


// "RefinedAbstraction"
class CustomersBusinessObject : BusinessObject
{
  
// Constructors
  public CustomersBusinessObject( string group )
    : 
base( group ){}

  
// Methods
  override public void ShowAll()
  
{
    
// Add separator lines
    Console.WriteLine();
    Console.WriteLine( 
"------------------------" );
    
base.ShowAll();
    Console.WriteLine( 
"------------------------" );
  }

}


// "Implementor"
abstract class DataObject
{
  
// Methods
  abstract public void NextRecord();
  
abstract public void PriorRecord();
  
abstract public void NewRecord( string name );
  
abstract public void DeleteRecord( string name );
  
abstract public void ShowRecord();
  
abstract public void ShowAllRecords();
}


// "ConcreteImplementor"
class CustomersDataObject : DataObject
{
  
// Fields
  private ArrayList customers = new ArrayList();
  
private int current = 0;

  
// Constructors
  public CustomersDataObject()
  
{
    
// Loaded from a database
    customers.Add( "Jim Jones" );
    customers.Add( 
"Samual Jackson" );
    customers.Add( 
"Allen Good" );
    customers.Add( 
"Ann Stills" );
    customers.Add( 
"Lisa Giolani" );
  }


  
// Methods
  public override void NextRecord()
  
{
    
if( current <= customers.Count - 1 )
      current
++;
  }


  
public override void PriorRecord()
  
{
    
if( current > 0 )
      current
--;
  }


  
public override void NewRecord( string name )
  
{
    customers.Add( name );
  }


  
public override void DeleteRecord( string name )
  
{
    customers.Remove( name );
  }


  
public override void ShowRecord()
  
{
    Console.WriteLine( customers[ current ] );
  }


  
public override void ShowAllRecords()
  
{
    
foreachstring name in customers )
      Console.WriteLine( 
" " + name );
  }

}


/// <summary>
/// Client test
/// </summary>

public class BusinessApp
{
  
public static void Main( string[] args )
  
{
    
// Create RefinedAbstraction
    CustomersBusinessObject customers =
      
new CustomersBusinessObject(" Chicago ");

    
// Set ConcreteImplementor
    customers.DataObject = new CustomersDataObject();

    
// Exercise the bridge
    customers.Show();
    customers.Next();
    customers.Show();
    customers.Next();
    customers.Show();
    customers.New( 
"Henry Velasquez" );

    customers.ShowAll();
  }

}

 

六、 在什么情况下应当使用桥梁模式

根据上面的分析,在以下的情况下应当使用桥梁模式:

  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。
  • 设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。
  • 一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。
  • 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。


参考文献:
阎宏,《Java与模式》,电子工业出版社
[美]James W. Cooper,《C#设计模式》,电子工业出版社
[美]Alan Shalloway  James R. Trott,《Design Patterns Explained》,中国电力出版社
[美]Robert C. Martin,《敏捷软件开发-原则、模式与实践》,清华大学出版社
[美]Don Box, Chris Sells,《.NET本质论 第1卷:公共语言运行库》,中国电力出版社

 

0
0
(请您对文章做出评价)
« 上一篇:不知道是不是Biztalk 2004的问题?
» 下一篇:设计模式(16)-Bridge Pattern
posted on 2004-11-11 15:01 吕震宇 阅读(19017) 评论(48)  编辑 收藏 网摘 所属分类: 设计模式

评论

#1楼 2004-11-11 16:33 mill2002
辛苦吕兄了!!!!!!!!!!!!!!!!!
收藏了慢慢看!谢谢!

  回复  引用    

#2楼 2004-11-12 11:18 Rover      
现在流行Ioc,完全的面向接口,比桥接好
  回复  引用  查看    

#3楼[楼主] 2004-11-12 14:37 吕震宇      
@Rover

呵呵,头一次听说Ioc,赶紧查资料。草草看了一遍后,感觉Ioc要求调用方与实现方最好是两个程序集,通过反射或者XML配置的方式实现“依赖注入”。能够更好的解耦。

我认为Ioc比Bridge更灵活,但正由于这个,会造成代码调用的效率比较低,毕竟要对配置进行解析或通过反射调用代码。所以,在对降低耦合要求更高的场景下,Ioc更合适。但普通应用,Bridge更简单高效一些。不知道我的理解对不对。

  回复  引用  查看    

#4楼 2004-11-12 18:49 Rover      
多看看Java社区,关注一下Spring.net,虽然Spring.net现在可以使用依赖注射,但aop的功能好像还没完成。Ioc和动态代理是实现Aop的一种手段,java现在是WebWork(或Struts)+Spring+Hibernate组合,.net下现在只有NHibernate可用。Ioc不一定要两个程序集,当要更换组件时修改配置文件就可以了

  回复  引用  查看    

#5楼[楼主] 2004-11-13 00:32 吕震宇      
实际上我早就想学学Java了,现在只能算知道个皮毛。有机会我会虚心向Java社区的朋友们多学习的。呵呵,Struts和Spring以前只是听说过,但从来没有去深入了解。
  回复  引用  查看    

#6楼 2004-11-15 00:37 寒枫天伤      
震宇,谢谢你给我的Biztalk的安装资料.表示十分感谢.

Rover,是否能够一介绍一下几个不错的java社区呢?

  回复  引用  查看    

#7楼 2004-12-03 15:09 粘粘
"显然,所有的调制解调器客户程序都必须更改。客户同意了对调制解调器客户程序的更改,因为他们别无选择。糟糕的是,现在必须要去告诉DedUser的编写者,他们必须要更改他们的代码!你可以想象他们听到这个会有多高兴。本来他们是不用调用dial的。"

为什么呢?DedicatedModem的Dial()等方法实现为空啊,而且本来就不需要拨号,为什么还要修改代码呢?


  回复  引用    

#8楼[楼主] 2004-12-03 21:45 吕震宇      
呵呵,我省略了原文中的一段话,其实有些客户抱怨直接连接的Modem经常受到别人的打扰,所以在Dial方法中添加了允许访问的功能,在HangUp方法中添加了禁止打扰的功能。这样就解决了问题,没想到又引来了新问题,就是“DedUser的编写者必须要更改他们的代码”。
  回复  引用  查看    

#9楼 2005-01-13 16:01 KingofSC
modem的例子令我看的头晕烟花,不知所云
google了一下bridge才总算有点明白
是不是modem的例子没说完整还是没说清楚呢

  回复  引用    

#10楼[楼主] 2005-01-13 20:20 吕震宇      
呵呵,没有说完整。完整的内容可以参考《敏捷软件开发-原则、模式与实践》。
  回复  引用  查看    

#11楼 2005-03-22 23:31 找路
不知道是不是我搞错了,上面画的桥梁模式结构图,其中implementor的的组合/聚合关系是不是应该连到RefinedAbstraction?下面的示意代码也是实例化RefinedAbstraction,我初学设计模式,说错了的话还请原谅。我看到c#设计模式里面的关系也是连到ListBridge上面的

  回复  引用    

#12楼 2005-06-28 12:45 cv222
这个模式的好处我感觉不出来, 范例看不明白, 吕震宇能不能写个范例,
包括不使用该模式的实现和使用该模式的实现, 做个对比, 相信很多人期待

  回复  引用    

#13楼[楼主] 2005-06-28 23:09 吕震宇      
@cv222

可以参考我的设计模式随笔中的文章《蜡笔与毛笔的故事》。
http://www.cnblogs.com/zhenyulu/articles/67016.html

  回复  引用  查看    

#14楼 2005-07-08 16:59 jotter[未注册用户]
“为了能拨国际电话号码、信用卡电话、PIN标识电话等等,必修对现有dial中使用char[10]存储号码改为能够拨打任意长度的电话号码。显然,所有的调制解调器客户程序都必须更改。”

dial方法不是已经封装在类里面了吗?为什么要改客户程序?

  回复  引用    

#15楼 2005-07-29 11:50 许多[未注册用户]
我的理解是: 桥接模式里面必然含有策略模式。
桥接模式可以和模板方法一起使用。

  回复  引用    

#16楼 2005-08-29 23:25 acoder[未注册用户]
是把问题简单化还是把问题复杂化?
如果为了使客户端不用修改,你设计了这个复杂的结构用了1天;
而如果是客户端只作简单修改,这个修改花了1小时;
那么你会怎么选择?

  回复  引用    

#17楼 2005-11-01 17:05 阿k[未注册用户]
昨晚看了GOF的bridge,一遍后只知道如何依葫蘆畫瓢。
今天搜到吕兄的这篇文章,只看了开头就通了。
写得好啊,吕兄。
造福万民,呵。

  回复  引用    

#18楼 2005-11-01 17:10 阿k[未注册用户]
spring一直在用,感觉它的依赖注入做到后面都公式化了。
没有“桥”用起来来得有趣。
感觉spring对我来说Hibernate事务管理的意义更大。
可能我理解得不深刻。

  回复  引用    

#19楼 2005-11-28 15:17 玉开      
感觉Bridge很像strategy,不知道对不对。

请震宇老师确认。

  回复  引用  查看    

#20楼[楼主] 2005-11-28 16:24 吕震宇      
@玉开

应当说是“貌似神离”。Strategy模式从表面上看似乎是Bridge的一个简化,相当于将桥的一条“腿”给去掉了,但两种模式应用场景和思想却是截然不同的。

Bridge强调的是两边可以自由变化,自由组合,而Strategy强调的是“算法”的可插入性。如果在我的设计模式随笔中用毛笔和蜡笔来形容Bridge模式的话,我想Strategy模式就更像可以更换头的改锥。

  回复  引用  查看    

#21楼 2005-12-22 19:39 honeysunny      
再第二段示例代码中看到
CustomersBusinessObject customers =
new CustomersBusinessObject(" Chicago ");

很奇怪为什么不用BusinessObject去声明 customers 呢?
BusinessObject customers =
new CustomersBusinessObject(" Chicago ");
有什么特殊的理由吗?


  回复  引用  查看    

"在类型层次结构具有多个自由度的情况中,BRIDGE模式通常是有用的。我们可以把这些层次结构分开并通过桥把它们结合到一起,而不是把它们合并起来。"

我的理解是相当于把乘法的复杂度通过桥模式转化成了加法的复杂度。

  回复  引用    

#23楼 2006-04-16 10:47 rayeuy[未注册用户]
我怎么没有觉得“调制解调器”例子中的bridge模式比adapter模式好了?
我觉得增加一种调制解调器后,物理上它只有一种使用方式,它只有一种它本身固有的连接方式(接口)。但是其他用户使用的接口与这个借口不同,因此要是其他用户使用该调制解调器就需要一个接口转换,很自然的就是adapter模式。
而bridge模式,以你的蜡笔与毛笔来说吧,当增加一种颜色后,实际上物理增加的是该种颜色的大中小号三支毛笔。由此可以看出bridge模式与adapter模式的区别。

小弟拙见,敬请指教!

  回复  引用    

#24楼 2006-04-17 10:19 rayeuy[未注册用户]
我觉得这里只是一个接口层面的问题,比如说加入了一个新的专用调制解调器,那么它必然实现了专用调制解调器的接口。前面我们已经有了一个从普通接口到专用接口的适配器,这个适配器依赖于专用调制解调器的接口,所以对于新加入的专用调制解调器不需要做任何修改,即可使用。
  回复  引用    

比如说采购时用桥接模式:有几类产品要采购,可以分配给几个供应商去下订单。这样可以灵活的选择哪类产品给哪家供应商下定单。不管你以后要采购的产品种类增加了,还是有了新的供应商。不知道如何把UML放上来。大家都把一些例子放在这里供学习,会对这种模式有更好的理解!
  回复  引用    

更通俗的理解:一组对象和另一组对象可以任意组合来完成一个动作。比如采购的例子,不同的产品为一组对象,不同的供应商为一组对象。它们两组对象之间可以任意组合来完成采购。最终UML图就像上面的调制解调器一样了!
  回复  引用    

#27楼 2006-05-05 16:39 ipiggg[未注册用户]

个人感觉Bridge在组合方面很牛X
楼主的毛笔和蜡笔很形象,谢谢。


  回复  引用    

楼主将Abstraction与Implementor之间的聚合关系画成组合关系了。^_^
  回复  引用    

在我看过的模式文章中,能够称得上深入浅出的我相信非你不可,浅显易懂的语言,恰当的例子把一个复杂的东西讲得非常透,先致敬再感谢。
  回复  引用    

#30楼 2007-01-29 09:58 PH580[未注册用户]
谢谢!
  回复  引用    

我的理解,“抽象”就是对象本身所固有的状态和行为,“实现”就是对象在不同环境中的不同表现,“抽象”和“实现”的解耦,就是将对象与环境无关的因素和与环境有关的因素分开实现。
  回复  引用    

#32楼 2008-03-11 21:46 木独猪      
有点像对象适配器模式,大家觉得呢?
  回复  引用  查看    

怎么感觉桥接模式有点像策略模式,只是多了一个抽象化层次,共同点都是用组合完成了类本身(抽象)和行为(具体)的分离~~
  回复  引用    

customers.DataObject = new CustomersDataObject();这样能编译通过吗 在类class BusinessObject
private DataObject dataObject
定义的是私有

abstraction.Implementor = new ConcreteImplementorA();这样能编译通过吗?
class Abstraction
{
// Fields
protected Implementor implementor;
}保护定义

  回复  引用    

#35楼 2009-02-26 11:50 younger[未注册用户]
弱弱地问句
老外为什么叫桥梁(Bridge)模式
桥的概念应该是将两个不能通信的对象连接起来。
和这个模式的内容完全没关系嘛
柄体(Handle and Body)模式或接口(Interface)模式
这两个说法还比较容易理解些

  回复  引用    

看了这么多模式,实质的东西就是面向对象的特性,抽象,继承,多态,封装,只不过根据各自的目的或作用不同,起了多个名字一样。
  回复  引用    

#37楼 2009-03-24 10:14 Clive_Li      
4年半前的文章,到现在依旧是一篇好文。
  回复  引用  查看