1. 定义
将抽象部分与实现部分分离,使它们都可以独立地变化。(抽象、实现、脱耦)
——《设计模式》GoF
- 抽象—存在于多个实体中的共同的概念性联系,就是抽象。作为一个过程,抽象就是忽略一些信息,从而把不同的实体当做同样的实体对待。
- 实现—针对抽象给出的具体实现。
- 所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦/解耦。在这里,脱耦是指将抽象和实现之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。
- 将两个角色之间的继承关系改为组合关系,就是将它们之间的强关联改换成为弱关联。因此,桥接模式中的所谓脱耦,就是指在一个软件系统的抽象和实现之间使用组合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥接模式的用意。
- 注意:桥接模式不能只认为是抽象和实现的分离,它其实并不仅限于此,更确切的理解: 应该是将一个事物中多个维度的变化分离。
2. 结构
- 抽象(Abstraction)角色:抽象给出通用操作接口定义,并保存一个对实现对象的引用。
- 精化的抽象(Refined Abstraction)角色:扩展抽象角色,给出接口实现。
- 实现(Implementor)角色:这个角色给出实现角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象角色的接口定义相同,实际上,这两个接口可以非常不一样。实现角色应当只给出底层操作,而抽象角色应当只给出基于底层操作的更高一层的操作(抽象角色调用实现角色的底层操作来完成其功能)。
- 具体实现(Concrete Implementor)角色:这个角色给出实现角色接口的具体实现。
3. 实例
假如我们需要开发一个同时支持PC和手机的坦克游戏,游戏在PC和手机上功能都一样,都有同样的类型,面临同样的功能需求变化,比如坦克可能有很多种不同的型号:T50,T75,T90等,将来还可能将这款游戏移植到TV中。
对于其中的坦克设计,我们可能很容易设计出来一个Tank的抽象基类,然后各种不同型号的Tank继承自该类;但是PC和手机上的图形绘制、声效、操作等实现完全不同……因此对于各种型号的坦克,都要提供各种不同平台上的坦克实现。
示例:TankGameInherit
如果用普通的继承结构,会导致类爆炸,再加一个型号需要添加好多类,典型的滥用继承,这种事我以前真干过…
桥接模式示例:TankGameBridge(老师给的C#源码)
-
Tank的型号和Tank的平台都继承自各自的抽象类,因此它们的变化都不会影响到对方。而它们之间的关联,我们使用组合的方式,把平台类放到Tank类中作为属性。这再次体现了组合优先于继承的思想。
-
桥接模式把平台的变化引出了基类Tank,使得Tank仅负责封装型号(抽象)的变化,而TankPlatformImplementation则负责封装平台(实现)的变化。应用程序在环境交互中使用的都是抽象类,并且把平台实现隐藏。
-
关键点: 其中Tank抽象类中定义tankImpl的地方就是一个组合。
namespace TankGameBridge
{
/// <summary>
/// 抽象角色
/// </summary>
public abstract class Tank
{
//桥接对象--调用实际实现对象的方法来完成下面的具体操作
protected TankPlatformImplementation tankImpl;
//老师这里直接在Tank类的构造函数中注入了TankPlatformImplementation平台实现类
//这意味着,客户端在调用T90/T50/T75时必须设置好平台实现类,这样使得两个抽象层之间是组合关系
//如果项目中有多于两个维度,使用桥接模式的时候可以将多个抽象之间的关系设置为聚合关系,需要实现调用方法注入也可以
public Tank(TankPlatformImplementation tankImpl) //创建Tank对象时需要指定具体的实现对象
{
this.tankImpl = tankImpl; //记住具体的实现对象,供子类方法调用
}
//坦克的基本特性,它们的实现将来依赖于tankImpl的具体方法
public abstract void Run();
public abstract void Turn(int direction);
public abstract void Shot();
}
}
namespace TankGameBridge
{
/// <summary>
/// 具体坦克类--精化的抽象
/// 调用内嵌的桥接对象实现具体型号的坦克的业务逻辑
/// </summary>
class T90:Tank
{
public T90(TankPlatformImplementation imp):base(imp) //构造子类对象时必须指定实现平台对象
{
}
public override void Run()
{
//调用具体实现对象(桥接对象)的方法完成Run操作
tankImpl.MoveTankTo(150, 250);
}
public override void Turn(int direction)
{
//调用具体实现对象(桥接对象)的方法完成Turn操作
}
public override void Shot()
{
//调用具体实现对象(桥接对象)的方法完成Shot操作
tankImpl.DoShot();
}
}
}
namespace TankGameBridge
{
/// <summary>
///坦克实现平台的抽象类----考虑到各个平台的差异,此处给出各个平台都能实现的最基本操作
/// </summary>
public abstract class TankPlatformImplementation{
//与抽象类Tank的基于游戏策略的基本操作不同,这里的基本操作将是在具体平台中实现Tank具体操作的基础
public abstract void MoveTankTo(int toX,int toY);
public abstract void DrawTank();
public abstract void DoShot();
}
}
namespace TankGameBridge
{
/// <summary>
/// 具体平台实现类---在PC机上实现Tank的基本操作
/// </summary>
class PcTankPlatformImplementation:TankPlatformImplementation {
public override void MoveTankTo(int toX, int toY){
//在PC机上实现坦克的基本移动操作
}
public override void DrawTank(){
//在PC机上实现坦克的基本绘制操作
}
public override void DoShot(){
//在PC机上实现坦克的基本射击操作
}
}
}
namespace TankGameBridge{
/// <summary>
/// 具体平台实现类--手机平台实现坦克的基本操作
/// </summary>
class MobileTankPlatformImplementation:TankPlatformImplementation{
public override void MoveTankTo(int toX, int toY){
//在手机上实现坦克的基本移动操作
}
public override void DrawTank(){
//在手机上实现坦克的基本绘制操作
}
public override void DoShot(){
//在手机上实现坦克的基本射击操作
}
}
}
/*客户端*/
namespace TankGameBridge
{
class Program
{
static void Main(string[] args)
{
//创建具体型号的坦克时同时指定使用的平台对象----则以后的代码中,基本动作的实现全部是采用该平台代码实现
Tank t50 = new T50(new PcTankPlatformImplementation());
//t50.xxxxx()
}
}
}
4.总结
- Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象(Tank的型号)和实现(不同的平台)可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,即“子类化”它们(比如不同的Tank型号子类,和不同的平台子类),得到各个子类之后,便可以任意组合它们,从而获得不同平台上的不同型号的实现。
- Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
- Bridge模式一般应用在“两个或多个非常强的变化维度”上,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换言之两个变化不会导致纵横交错的结果,并不一定要使用Bridge模式。
- 桥接模式并不同于适配器模式,适配器模式其实是一个事后诸葛亮,当发现以前的东西不适用了才去做一个弥补的措施。桥接模式相对来说所做的改变比适配器模式早(用抽象的接口封装了实现的接口),它可以适用于封装有两个甚至两个以上维度的变化。