.NET设计模式(9):桥接模式(Bridge Pattern)

 

桥接模式(Bridge Pattern

——.NET设计模式系列之九

Terrylee20062

概述

在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?这就要使用Bridge模式。

意图

将抽象部分与实现部分分离,使它们都可以独立的变化。[GOF 《设计模式》]

结构图

1 Bridge模式结构图

生活中的例子

桥接模式将抽象部分与它的实现分离,使它们能够独立地变化。一个普通的开关控制的电灯、电风扇等等,都是桥接的例子。开关的目的是将设备打开或关闭。实际的开关可以是简单的双刀拉链开关,也可以是调光开关。

2 使用电子开关例子的桥接对象图

桥接模式解说

在创建型模式里面,我曾经提到过抽象与实现,抽象不应该依赖于具体实现细节,实现细节应该依赖于抽象。看下面这幅图:

3  抽象不应该依赖于实现细节

在这种情况下,如果抽象B稳定,而实现细节b变化,这时用创建型模式来解决没有问题。但是如果抽象B也不稳定,也是变化的,该如何解决?这就要用到Bridge模式了。

我们仍然用日志记录工具这个例子来说明Bridge模式。现在我们要开发一个通用的日志记录工具,它支持数据库记录DatabaseLog和文本文件记录FileLog两种方式,同时它既可以运行在.NET平台,也可以运行在Java平台上。

根据我们的设计经验,应该把不同的日志记录方式分别作为单独的对象来对待,并为日志记录类抽象出一个基类Log出来,各种不同的日志记录方式都继承于该基类:

4 Log类结构图

实现代码如下:

public abstract class Log

{

    public abstract void Write(string log);

}

public class DatabaseLog : Log

{

    public override void Write(string log)

    {

        //......Log Database

    }

}

public class TextFileLog : Log

{

    public override void Write(string log)

    {

       //......Log Text File

    }

}

另外考虑到不同平台的日志记录,对于操作数据库、写入文本文件所调用的方式可能是不一样的,为此对于不同的日志记录方式,我们需要提供各种不同平台上的实现,对上面的类做进一步的设计得到了下面的结构图:

5

实现代码如下:

public class NDatabaseLog : DatabaseLog

{

    public override void Write(string log)

    {

        //......(.NET平台)Log Database

    }

}

public class JDatabaseLog : DatabaseLog

{

    public override void Write(string log)

    {

        //......(Java平台)Log Database

    }

}

public class NTextFileLog : TextFileLog

{

    public override void Write(string log)

    {

        //......(.NET平台)Log Text File

    }

}

public class JTextFileLog : TextFileLog

{

    public override void Write(string log)

    {

        //......(Java平台)Log TextFile

    }

}

现在的这种设计方案本身是没有任何错误的,假如现在我们要引入一种新的xml文件的记录方式,则上面的类结构图会变成:

6

如图中蓝色的部分所示,我们新增加了一个继承于Log基类的子类,而没有修改其它的子类,这样也符合了开放-封闭原则。如果我们引入一种新的平台,比如说我们现在开发的日志记录工具还需要支持Borland平台,此时该类结构又变成了:

7

同样我们没有修改任何的东西,只是增加了两个继承于DatabaseLogTextFileLog的子类,这也符合了开放-封闭原则。

但是我们说这样的设计是脆弱的,仔细分析就可以发现,它还是存在很多问题,首先它在遵循开放-封闭原则的同时,违背了类的单一职责原则,即一个类只有一个引起它变化的原因,而这里引起Log类变化的原因却有两个,即日志记录方式的变化和日志记录平台的变化;其次是重复代码会很多,不同的日志记录方式在不同的平台上也会有一部分的代码是相同的;再次是类的结构过于复杂,继承关系太多,难于维护,最后最致命的一点是扩展性太差。上面我们分析的变化只是沿着某一个方向,如果变化沿着日志记录方式和不同的运行平台两个方向变化,我们会看到这个类的结构会迅速的变庞大。

现在该是Bridge模式粉墨登场的时候了,我们需要解耦这两个方向的变化,把它们之间的强耦合关系改成弱联系。我们把日志记录方式和不同平台上的实现分别当作两个独立的部分来对待,对于日志记录方式,类结构图仍然是:

8

现在我们引入另外一个抽象类ImpLog,它是日志记录在不同平台的实现的基类,结构图如下:

9

实现代码如下:

public abstract class ImpLog

{

    public abstract void Execute(string msg);

}

public class NImpLog : ImpLog

{

    public override void Execute(string msg)

    {

        //...... .NET平台

    }

}

public class JImpLog : ImpLog

{

    public override void Execute(string msg)

    {

        //...... Java平台

    }

}

这时对于日志记录方式和不同的运行平台这两个类都可以独立的变化了,我们要做的工作就是把这两部分之间连接起来。那如何连接呢?在这里,Bridge使用了对象组合的方式,类结构图如下:

10

实现代码如下:
public abstract class Log

{

    protected ImpLog implementor;

 

    public ImpLog Implementor

    {

        set { implementor = value; }   

    }

    public virtual void Write(string log)

    {

        implementor.Execute(log);

    }

}

public class DatabaseLog : Log

{

    public override void Write(string log)

    {

        implementor.Execute(log);

    }

}

public class TextFileLog : Log

{

    public override void Write(string log)

    {

        implementor.Execute(log);

    }

}

可以看到,通过对象组合的方式,Bridge模式把两个角色之间的继承关系改为了耦合的关系,从而使这两者可以从容自若的各自独立的变化,这也是Bridge模式的本意。再来看一下客户端如何去使用:

class App

{

    public static void Main(string[] args)

    {

        //.NET平台下的Database Log

        Log dblog = new DatabaseLog();

        dblog.Implementor = new NImpLog();

        dblog.Write();

       

        //Java平台下的Text File Log

        Log txtlog = new TextFileLog();

        txtlog.Implementor = new JImpLog();

        txtlog.Write();

    }

}

可能有人会担心说,这样不就又增加了客户程序与具体日志记录方式之间的耦合性了吗?其实这样的担心是没有必要的,因为这种耦合性是由于对象的创建所带来的,完全可以用创建型模式去解决,就不是这里我们所讨论的内容了。

最后我们再来考虑一个问题,为什么Bridge模式要使用对象组合的方式而不是用继承呢?如果采用继承的方式,则Log类,ImpLog类都为接口,类结构图如下:

11

实现代码如下:

public class NDatabaseLog : DatabaseLog, IImpLog

{

    //......

}

public class JDatabaseLog : DatabaseLog, IImpLog

{

    //......

}

public class NTextFileLog : TextFileLog, IImpLog

{

    //......

}

public class JTextFileLog : TextFileLog, IImpLog

{

    //......

}

如上图中蓝色的部分所示,它们既具有日志记录方式的特性,也具有接口IimpLog的特性,它已经违背了面向对象设计原则中类的单一职责原则,一个类应当仅有一个引起它变化的原因。所以采用Bridge模式往往是比采用多继承更好的方案。说到这里,大家应该对Bridge模式有一些认识了吧?如果在开发中遇到有两个方向上纵横交错的变化时,应该能够想到使用Bridge模式,当然了,有时候虽然有两个方向上的变化,但是在某一个方向上的变化并不是很剧烈的时候,并不一定要使用Bridge模式。

效果及实现要点

1Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。

2.所谓抽象和实现沿着各自维度的变化,即“子类化”它们,得到各个子类之后,便可以任意它们,从而获得不同平台上的不同型号。

3Bridge模式有时候类似于多继承方案,但是多继承方案往往违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。

4Bridge模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换言之两个变化不会导致纵横交错的结果,并不一定要使用Bridge模式。

适用性

在以下的情况下应当使用桥梁模式:

1.如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。

2.设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。

3.一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。

4.虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。

总结

Bridge模式是一个非常有用的模式,也非常复杂,它很好的符合了开放-封闭原则和优先使用对象,而不是继承这两个面向对象原则。

参考资料

阎宏,《Java与模式》,电子工业出版社

James W. Cooper,《C#设计模式》,电子工业出版社

Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社

MSDN WebCast C#面向对象设计模式纵横谈(8)Bridge桥接模式(结构型模式)

posted on 2006-02-24 09:06 TerryLee 阅读(11680) 评论(55)  编辑 收藏 所属分类: Design Patterns

评论

#1楼  2006-02-24 10:03 ZO [未注册用户]

分析的很透彻!!!
学习中……   回复  引用  查看    

#2楼  2006-02-24 10:16 源码工作室      

越来越佩服Terrylee兄了。
有空想和你聊聊,我想有共同兴趣的人肯定能聊的很投机的。
很想教你这个朋友。   回复  引用  查看    

#3楼 [楼主] 2006-02-24 10:25 Terrylee      

@源码工作室
呵呵,我也很想交你这个朋友
我的MSN是lhj_cauc@hotmail.com
有机会我们多多交流~~~~   回复  引用  查看    

#4楼  2006-02-24 12:02 quitgame      

很不错,受益匪浅

但是 个人觉得没有能看到桥的存在,桥应该在抽象类上或者第三个类上.   回复  引用  查看    

#5楼 [楼主] 2006-02-24 12:39 Terrylee      

@quitgame
可能我在表述的过程中还有些地方没有点透,说得不够清楚~~~   回复  引用  查看    

#6楼  2006-02-24 14:27 A.Z      

请看httpwebrequest   回复  引用  查看    

#7楼 [楼主] 2006-02-24 15:41 Terrylee      

@A.Z
HttpWebRequest中使用了桥接模式了吗?
我只知道它里面应用了工厂方法模式。
如果你有这方面的资料,不妨介绍一下,共同学习!   回复  引用  查看    

#8楼  2006-02-25 10:39 M [未注册用户]

能把设计模式分析的如此透彻的文章少见,期待……   回复  引用  查看    

#9楼  2006-02-25 22:47 劲舞团 [未注册用户]

不错。。。收藏了。。。。   回复  引用  查看    

#10楼  2006-02-27 09:24 开心糊涂      

谢谢分享   回复  引用  查看    

#11楼  2006-02-28 15:37 mrhgw [未注册用户]


有一点疑问,就以上面的例子:
方式一:
Log dblog = new DatabaseLog();
dblog.Implementor = new NImpLog();
dblog.Write();

方式二:
Log dblog = new DatabaseLog();
dblog.Implementor = new JImpLog();
dblog.Write();

方式三:
//Java平台下的Text File Log
Log txtlog = new TextFileLog();
txtlog.Implementor = new JImpLog();
txtlog.Write();

方式四:
Log txtlog = new TextFileLog();
txtlog.Implementor = new NImpLog();
txtlog.Write();

从实际调用来说(我的理解),方法一和方法四应该是调用同一个方法;方法二和方法三是调用同一个方法?我想知道的是类ImpLog及其继承类是如何确定写入方式的(即文本还是数据库),请高手解答,谢了。   回复  引用  查看    

#12楼 [楼主] 2006-03-01 12:49 Terrylee      

@mrhgw
方法一和方法四都是.NET平台上的方法,而方法三和方法二都是Java平台上的,

具体写入日志的方式是在客户程序中确定的……   回复  引用  查看    

#13楼  2006-03-03 19:22 overred      

工厂模式是桥模式的一个特定的实现
嘿嘿
还是希望Terrylee 多产   回复  引用  查看    

#14楼  2006-03-15 12:58 突破自己      

请用桥接模式来改PetShop3.0的数据访问层   回复  引用  查看    

#15楼 [楼主] 2006-03-16 08:41 Terrylee      

@突破自己
PetShop3.0的数据访问层有必要用桥模式吗??   回复  引用  查看    

#16楼  2006-03-17 12:36 突破自己      


@Terrylee
请看
http://weblogs.asp.net/afilev/archive/2003/06/22/9102.aspx   回复  引用  查看    

#17楼 [楼主] 2006-03-17 13:43 Terrylee      

@突破自己
也是一种实现思路,但是有问题。

有时间我会试着改一下的:)   回复  引用  查看    

#18楼  2006-06-11 11:37 过客111 [未注册用户]

感觉这里的用法和适配器几乎一样,两者除了说法上不同,本质有什么区别?   回复  引用  查看    

#19楼  2006-06-25 15:44 肖鹏      

@mrhgw
方式一和方式四调用的并不是同一个方法,方式二和方式三调用的也不是同一个方法。我也曾经在这一点上面有疑问。其实这里的关键是DatabaseLog和TextFileLog了解自己应该调用IImpleLog的哪个函数,也就是从函数名上说方式一和方式二调用的同一个函数名,方式三和方式四调用的是同一个函数名。从函数实现上说,这四个方式其实调用了四个不同的函数实现。你可以看看我的一篇文章:http://designpatterns.cnblogs.com/archive/2006/06/17/bridge_design_patterns.html。这是我的理解,很希望与大家交个朋友,多跟大家交流共同进步。   回复  引用  查看    

#20楼 [楼主] 2006-06-26 08:44 TerryLee      

@肖鹏
你的那篇文章写的不错,以后可以多多交流:-)   回复  引用  查看    

#21楼  2006-06-26 10:36 肖鹏      

@TerryLee
谢谢,其实对于mrhgw的问题我也有疑问,就像我在我的文章最后写的“不过这里面还是有一点让人不舒服的地方,就是Editor中包含的两个方法,实际上对应了两种Editable。也许是这个例子举的不好。但是《Design patterns explained》中的例子和本例大同小异。”不知道你是怎么看待这个问题的?   回复  引用  查看    

#22楼  2006-06-30 12:52 3322 [未注册用户]

我是初学者,看了一天早上你写的文章,感觉很不错,但是有一些暂时还不能理解,打算以后再看一遍,非常感谢!!!!   回复  引用  查看    

#23楼 [楼主] 2006-06-30 13:00 TerryLee      

@3322

不用客气,以后多交流:-)   回复  引用  查看    

#24楼  2006-08-17 14:59 txd_lf [未注册用户]

肖鹏说的没错,ImpLog至少应该有两个方法,一个是实现数据库的操作WriteDB,一个是实现文件的操作WriteTxt,ImpLog只是针对实现平台所提供的一些比较低级的操作。然后由Log的子类来决定调用那个。

而且ImgLog的子类不应该是.NET和Java平台的延伸(他们只是开发平台,而不是运行平台),而应该是Windows平台和Unix平台。   回复  引用  查看    

#25楼  2006-10-16 14:41 Frank Huang [未注册用户]

同意txd_lf的观点,这里我发表下个人的看法(如有不对之处希望大家指出)。现在我把ImpLog的代码应该改成:
public abstract class ImpLog

{

public abstract void WriteDB(string msg);
public abstract void WriteTxt(string msg);

}


public class NImpLog : ImpLog

{

public override void WriteDB(string msg)

{

//...... .NET平台下写DB

}

public override void WriteTxt(string msg)

{

//...... .NET平台下写Text

}

}


public class JImpLog : ImpLog

{

public override void WriteDB(string msg)

{

//...... JAVA平台下写DB

}

public override void WriteTxt(string msg)

{

//...... JAVA平台下写Text

}

}

现在我的问题是这样的,bridge pattern实际上是把M*N的继承关系转化成了M继承关系+N聚集关系,这样的转换在M和N易变的情况下是很有必要的。不过我同时发现,这样的转换却不得不增加ImpLog接口的复杂度,从我修改后的代码不难看出,M*N的继承关系实际上是被M*N个函数实现代替了(现在每各异个实现ImpLog的类都需要实现WriteDB和W