认识桥接模式

在桥接模式里面,不太好理解的就是桥接的概念,什么事桥接?为何需要桥接?如何桥接?把这几个问题搞清楚,就基本明白桥接的含义了。

一个个来,什么是桥接?所谓桥接,通俗点说就是再不同的东西之间搭一个桥,让他们能够连接起来,可以相互通讯和使用。那么在桥接模式中到底是给什么东西来搭桥呢?就是为被分离了的抽象部分和实现部分来搭桥。

但是要注意:在桥接模式中的桥接是单向的,也就是只能是抽象部分的对象去使用具体实现部分的对象,而不能反过来,也就是单向桥。

为何需要桥接?

为了达到让抽象部分和实现部分都可以独立变化的目的,在桥接模式中,是把抽象部分和实现部分分离开来的,虽然从程序结构上是分开了,但是在抽象部分实现的时候,还是需要使用具体的实现的。抽象部分如何才能调用具体实现部分的功能呢?

如何桥接?

只要让抽象部分拥有实现部分的接口对象,这就桥接上了,在抽象部分就可以通过这个接口来调用具体实现部分的功能。也就是说,桥接在程序上就是体现成了在抽象部分拥有实现部分的接口对象,维护桥接就是维护这个关系。

独立变化?

桥接模式的意图:使得抽象和实现可以独立变化,都可以分别扩充。也就是说抽象部分和实现部分是一种非常松散的关系,从某个角度来讲,抽象部分和实现部分是可以完全分开的,独立的,抽象部分不过是一个使用实现部分对外接口的程序罢了。

桥接模式和继承

继承是扩展对象功能的一种常见手段,通常情况下,继承扩展的功能变化纬度都是一纬的,也就是变化的因素只有一类。

对于出现变化因素有两类的,也就是有两个变化纬度的情况,继承实现就会比较痛苦。比如上面的示例,就有两个变化纬度,一个是消息的类别,不同的消息类别处理不同;另外一个是消息的发送方式。

从理论上来说,如果用继承的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的乘积。比如上面的示例,在消息类别的纬度上,目前的可变数量是3个,普通消息,加急消息和特急消息;在消息发送方式的纬度上,目前的可变数量也是3个,站内短消息,Email和手机短消息。这种情况下,如果要实现全的话,那么需要的实现类应该是:3X3=9个。

而桥接模式就是用来解决这种有两个变化纬度的情况下,如何灵活的扩展功能的一个很好的方案。其实,桥接模式主要是把继承改成了使用对象组合,从而把两个纬度分开,让每一个纬度单独变化,最后通过对象组合的方式,把两个纬度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效的减少了实际实现的类的个数。

从理论上来说,如果使用桥接模式的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的和。

桥接模式的时序图:

bridge_01

谁来桥接

所谓谁来桥接,就是谁来负责创建抽象部分和实现部分的关系,说得更直白点,就是谁来负责创建Implementor的对象,并把它设置到抽象部分的对象中去,这点对于使用桥接模式来说,是十分重要的。

大致有如下几种实现方式:

由客户端负责创建Implementor的对象,并在创建抽象部分的时候,把它设置到抽象部分的对象里面去,前面的示例采用的就是这个方式。

可以在Abstraction中选择并创建一个缺省的Implementor的对象,然后子类可以根据需要改变这个实现。

也可以使用抽象工厂或者简单工厂来选择并创建具体的Implementor的对象,抽象部分的类可以通过调用工厂的方法来获取Implementor的对象。

如果使用Ioc/DI的话,还可以通过Ioc/DI容器来创建具体的Implementor的对象,并注入到Abstration中。

典型例子-JDBC

在Java应用中,对于桥接模式有一个非常典型的例子,就是:应用程序使用JDBC驱动程序进行开发的方式。所谓驱动程序,指的是按照预先约定好的接口来操作计算机系统或者是外围设备的程序。
先简单的回忆一下使用JDBC进行开发的过程,简单的片断代码示例如下:

   1: String sql = "具体要操作的sql语句";
   2:         // 1:装载驱动
   3:         Class.forName("驱动的名字");
   4:         // 2:创建连接
   5:         Connection conn = DriverManager.getConnection(
   6: "连接数据库服务的URL", "用户名","密码");
   7:  
   8:         // 3:创建statement或者是preparedStatement
   9:         PreparedStatement pstmt = conn.prepareStatement(sql);
  10:         // 4:执行sql,如果是查询,再获取ResultSet
  11:         ResultSet rs = pstmt.executeQuery(sql);
  12:  
  13:         // 5:循环从ResultSet中把值取出来,封装到数据对象中去
  14:         while (rs.next()) {
  15:             // 取值示意,按名称取值
  16:             String uuid = rs.getString("uuid");
  17:             // 取值示意,按索引取值
  18:             int age = rs.getInt(2);
  19:         }
  20:         //6:关闭
  21:         rs.close();
  22:         pstmt.close();
  23:         conn.close();

从上面的示例可以看出,我们写的应用程序,是面向JDBC的API在开发,这些接口就相当于桥接模式中的抽象部分的接口。那么怎样得到这些API的呢?是通过DriverManager来得到的。此时的系统结构如图9所示:


图9 基于JDBC开发的应用程序结构示意图

那么这些JDBC的API,谁去实现呢?光有接口,没有实现也不行啊。
该驱动程序登场了,JDBC的驱动程序实现了JDBC的API,驱动程序就相当于桥接模式中的具体实现部分。而且不同的数据库,由于数据库实现不一样,可执行的Sql也不完全一样,因此对于JDBC驱动的实现也是不一样的,也就是不同的数据库会有不同的驱动实现。此时驱动程序这边的程序结构如图10所示:

图10 驱动程序实现结构示意图

有了抽象部分——JDBC的API,有了具体实现部分——驱动程序,那么它们如何连接起来呢?就是如何桥接呢?
就是前面提到的DriverManager来把它们桥接起来,从某个侧面来看,DriverManager在这里起到了类似于简单工厂的功能,基于JDBC的应用程序需要使用JDBC的API,如何得到呢?就通过DriverManager来获取相应的对象。
那么此时系统的整体结构如图11所示:

图11 JDBC的结构示意图

通过上图可以看出,基于JDBC的应用程序,使用JDBC的API,相当于是对数据库操作的抽象的扩展,算作桥接模式的抽象部分;而具体的接口实现是由驱动来完成的,驱动这边自然就相当于桥接模式的实现部分了。而桥接的方式,不再是让抽象部分持有实现部分,而是采用了类似于工厂的做法,通过DriverManager来把抽象部分和实现部分对接起来,从而实现抽象部分和实现部分解耦。

JDBC的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。而且,JDBC的这种架构,还合理的划分了应用程序开发人员和驱动程序开发人员的边界。
对于有些朋友会认为,从局部来看,体现了策略模式,比如在上面的结构中去掉“JDBC的API和基于JDBC的应用程序”这边,那么剩下的部分,看起来就是一个策略模式的体现。此时的DriverManager就相当于上下文,而各个具体驱动的实现就相当于是具体的策略实现,这个理解也不算错,但是在这里看来,这么理解是比较片面的。
对于这个问题,再次强调一点:对于设计模式,要从整体结构上、从本质目标上、从思想体现上来把握,而不要从局部、从表现、从特例实现上来把握。

posted on 2011-12-16 23:43  洛克恩  阅读(375)  评论(1)    收藏  举报