研磨设计模式之 桥接模式-3

3  模式讲解

3.1  认识桥接模式

(1)什么是桥接
        在桥接模式里面,不太好理解的就是桥接的概念,什么是桥接?为何需要桥接?如何桥接?把这些问题搞清楚了,也就基本明白桥接的含义了。
        一个一个来,先看什么是桥接?所谓桥接,通俗点说就是在不同的东西之间搭一个桥,让他们能够连接起来,可以相互通讯和使用。那么在桥接模式中到底是给什么东西来搭桥呢?就是为被分离了的抽象部分和实现部分来搭桥,比如前面示例中抽象的消息和具体消息发送之间搭个桥。
        但是这里要注意一个问题:在桥接模式中的桥接是单向的,也就是只能是抽象部分的对象去使用具体实现部分的对象,而不能反过来,也就是个单向桥。


(2)为何需要桥接
        为了达到让抽象部分和实现部分都可以独立变化的目的,在桥接模式中,是把抽象部分和实现部分分离开来的,虽然从程序结构上是分开了,但是在抽象部分实现的时候,还是需要使用具体的实现的,这可怎么办呢?抽象部分如何才能调用到具体实现部分的功能呢?很简单,搭个桥不就可以了,搭个桥,让抽象部分通过这个桥就可以调用到实现部分的功能了,因此需要桥接。

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


(4)独立变化
        桥接模式的意图:使得抽象和实现可以独立变化,都可以分别扩充。也就是说抽象部分和实现部分是一种非常松散的关系,从某个角度来讲,抽象部分和实现部分是可以完全分开的,独立的,抽象部分不过是一个使用实现部分对外接口的程序罢了。
        如果这么看桥接模式的话,就类似于策略模式了,抽象部分需要根据某个策略,来选择真实的实现,也就是说桥接模式的抽象部分相当于策略模式的上下文。更原始的就直接类似于面向接口编程,通过接口分离的两个部分而已。但是别忘了,桥接模式的抽象部分,是可以继续扩展和变化的,而策略模式只有上下文,是不存在所谓抽象部分的。
        那抽象和实现为何还要组合在一起呢?原因是在抽象部分和实现部分还是存在内部联系的,抽象部分的实现通常是需要调用实现部分的功能来实现的。


(5)动态变换功能
        由于桥接模式中的抽象部分和实现部分是完全分离的,因此可以在运行时动态组合具体的真实实现,从而达到动态变换功能的目的。
        从另外一个角度看,抽象部分和实现部分没有固定的绑定关系了,因此同一个真实实现可以被不同的抽象对象使用,反过来,同一个抽象也可以有多个不同的实现。就像前面示例的那样,比如:站内短消息的实现功能,可以被普通消息、加急消息或是特急消息等不同的消息对象使用;反过来,某个消息具体的发送方式,可以是站内短消息,或者是Email,也可以是手机短消息等具体的发送方式。

(6)退化的桥接模式
        如果Implementor仅有一个实现,那么就没有必要创建Implementor接口了,这是一种桥接模式退化的情况。这个时候Abstraction和Implementor是一对一的关系,虽然如此,也还是要保持它们的分离状态,这样的话,它们才不会相互影响,才可以分别扩展。
        也就是说,就算不要Implementor接口了,也要保持Abstraction和Implementor是分离的,模式的分离机制仍然是非常有用的。

(7)桥接模式和继承
        继承是扩展对象功能的一种常见手段,通常情况下,继承扩展的功能变化纬度都是一纬的,也就是变化的因素只有一类。
        对于出现变化因素有两类的,也就是有两个变化纬度的情况,继承实现就会比较痛苦。比如上面的示例,就有两个变化纬度,一个是消息的类别,不同的消息类别处理不同;另外一个是消息的发送方式。
        从理论上来说,如果用继承的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的乘积那么多个。比如上面的示例,在消息类别的纬度上,目前的可变数量是3个,普通消息、加急消息和特急消息;在消息发送方式的纬度上,目前的可变数量也是3个,站内短消息、Email和手机短消息。这种情况下,如果要实现全的话,那么需要的实现类应该是:3 X 3 = 9个。
        如果要在任何一个纬度上进行扩展,都需要实现另外一个纬度上的可变数量那么多个实现类,这也是为何会感到扩展起来很困难。而且随着程序规模的加大,会越来越难以扩展和维护。
        而桥接模式就是用来解决这种有两个变化纬度的情况下,如何灵活的扩展功能的一个很好的方案。其实,桥接模式主要是把继承改成了使用对象组合,从而把两个纬度分开,让每一个纬度单独去变化,最后通过对象组合的方式,把两个纬度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效的减少了实际实现的类的个数。
        从理论上来说,如果用桥接模式的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的和那么多个。同样是上面那个示例,使用桥接模式来实现,实现全的话,最后需要的实现类的数目应该是:3 + 3 = 6个。
        这也从侧面体现了,使用对象组合的方式比继承要来得更灵活。


(8)桥接模式的调用顺序示意图
        桥接模式的调用顺序如图8所示:


                      图8  桥接模式的调用顺序示意图

3.2  谁来桥接

        所谓谁来桥接,就是谁来负责创建抽象部分和实现部分的关系,说得更直白点,就是谁来负责创建Implementor的对象,并把它设置到抽象部分的对象里面去,这点对于使用桥接模式来说,是十分重要的一点。
        大致有如下几种实现方式:

  • 由客户端负责创建Implementor的对象,并在创建抽象部分的对象的时候,把它设置到抽象部分的对象里面去,前面的示例采用的就是这个方式
  • 可以在抽象部分的对象构建的时候,由抽象部分的对象自己来创建相应的Implementor的对象,当然可以给它传递一些参数,它可以根据参数来选择并创建具体的Implementor的对象
  • 可以在Abstraction中选择并创建一个缺省的Implementor的对象,然后子类可以根据需要改变这个实现
  • 也可以使用抽象工厂或者简单工厂来选择并创建具体的Implementor的对象,抽象部分的类可以通过调用工厂的方法来获取Implementor的对象
  • 如果使用IoC/DI容器的话,还可以通过IoC/DI容器来创建具体的Implementor的对象,并注入回到Abstraction中

下面分别给出后面几种实现方式的示例。
1:由抽象部分的对象自己来创建相应的Implementor的对象
        对于这种情况的实现,又分成两种,一种是需要外部传入参数,一种是不需要外部传入参数。

       (1)从外面传递参数比较简单,比如前面的示例,如果用一个type来标识具体采用哪种发送消息的方案,然后在Abstraction的构造方法中,根据type进行创建就好了。

        还是代码示例一下,主要修改Abstraction的构造方法,示例代码如下:

/**
 * 抽象的消息对象
 */
public abstract class AbstractMessage {
	/**
	 * 持有一个实现部分的对象
	 */
	protected MessageImplementor impl;
	/**
	 * 构造方法,传入选择实现部分的类型 
	 * @param type 传入选择实现部分的类型
	 */
	public AbstractMessage(int type){
		if(type==1){
			this.impl = new MessageSMS();
		}else if(type==2){
			this.impl = new MessageEmail();
		}else if(type==3){
			this.impl = new MessageMobile();
		}
	}	
	/**
	 * 发送消息,转调实现部分的方法
	 * @param message 要发送的消息内容
	 * @param toUser 把消息发送的目的人员
	 */
	public void sendMessage(String message,String toUser){
		this.impl.send(message, toUser);
	}	
}

 

(2)对于不需要外部传入参数的情况,那就说明是在Abstraction的实现中,根据具体的参数数据来选择相应的Implementor对象。有可能在Abstraction的构造方法中选,也有可能在具体的方法中选。
        比如前面的示例,如果发送的消息长度在100以内采用手机短消息,长度在100-1000采用站内短消息,长度在1000以上采用Email,那么就可以在内部方法中自己判断实现了。
        实现中,大致有如下改变:

  • 原来protected的MessageImplementor类型的属性,不需要了,去掉
  • 提供一个protected的方法来获取要使用的实现部分的对象,在这个方法里面,根据消息的长度来选择合适的实现对象
  • 构造方法什么都不用做了,也不需要传入参数
  • 在原来使用impl属性的地方,要修改成通过上面那个方法来获取合适的实现对象了,不能直接使用impl属性,否则会没有值

示例代码如下:

public abstract class AbstractMessage {
	/**
	 * 构造方法
	 */
	public AbstractMessage(){
		//现在什么都不做了
	}
	/**
	 * 发送消息,转调实现部分的方法
	 * @param message 要发送的消息内容
	 * @param toUser 把消息发送的目的人员
	 */
	public void sendMessage(String message,String toUser){		
		this.getImpl(message).send(message, toUser);
	}
/**
	 * 根据消息的长度来选择合适的实现
	 * @param message 要发送的消息
	 * @return 合适的实现对象
	 */
	protected MessageImplementor getImpl(String message) {
		MessageImplementor impl = null;
		if(message == null){
			//如果没有消息,默认使用站内短消息
			impl = new MessageSMS();
		}else if(message.length()< 100){
			//如果消息长度在100以内,使用手机短消息
			impl = new MessageMobile();
		}else if(message.length()<1000){
			//如果消息长度在100-1000以内,使用站内短消息
			impl = new MessageSMS();
		}else{
			//如果消息长度在1000以上
			impl = new MessageEmail();
		}
		return impl;
	}
}

(3)小结一下
        对于由抽象部分的对象自己来创建相应的Implementor的对象的这种情况,不管是否需要外部传入参数,优点是客户使用简单,而且集中控制Implementor对象的创建和切换逻辑;缺点是要求Abstraction知道所有的具体的Implementor实现,并知道如何选择和创建它们,如果今后要扩展Implementor的实现,就要求同时修改Abstraction的实现,这会很不灵活,使扩展不方便。

2:在Abstraction中创建缺省的Implementor对象
        对于这种方式,实现比较简单,直接在Abstraction的构造方法中,创建一个缺省的Implementor对象,然后子类根据需要,看是直接使用还是覆盖掉。示例代码如下:

public abstract class AbstractMessage {
	protected MessageImplementor impl;
	/**
	 * 构造方法
	 */
	public AbstractMessage(){
		//创建一个默认的实现
		this.impl = new MessageSMS();
	}
	public void sendMessage(String message,String toUser){
		this.impl.send(message, toUser);
	}
}

 这种方式其实还可以使用工厂方法,把创建工作延迟到子类。


3:使用抽象工厂或者是简单工厂
        对于这种方式,根据具体的需要来选择,如果是想要创建一系列实现对象,那就使用抽象工厂,如果是创建单个的实现对象,那就使用简单工厂就可以了。
        直接在原来创建Implementor对象的地方,直接调用相应的抽象工厂或者是简单工厂,来获取相应的Implementor对象,很简单,这个就不去示例了。
        这种方法的优点是Abstraction类不用和任何一个Implementor类直接耦合。


4:使用IoC/DI的方式
        对于这种方式,Abstraction的实现就更简单了,只需要实现注入Implementor对象的方法就可以了,其它的Abstraction就不管了。
        IoC/DI容器会负责创建Implementor对象,并设置回到Abstraction对象中,使用IoC/DI的方式,并不会改变Abstraction和Implementor的关系,Abstraction同样需要持有相应的Implementor对象,同样会把功能委托给Implementor对象去实现。


3.3  典型例子-JDBC

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

String sql = "具体要操作的sql语句";
		// 1:装载驱动
		Class.forName("驱动的名字");
		// 2:创建连接
		Connection conn = DriverManager.getConnection(
"连接数据库服务的URL", "用户名","密码");

		// 3:创建statement或者是preparedStatement
		PreparedStatement pstmt = conn.prepareStatement(sql);
		// 4:执行sql,如果是查询,再获取ResultSet
		ResultSet rs = pstmt.executeQuery(sql);

		// 5:循环从ResultSet中把值取出来,封装到数据对象中去
		while (rs.next()) {
			// 取值示意,按名称取值
			String uuid = rs.getString("uuid");
			// 取值示意,按索引取值
			int age = rs.getInt(2);
		}
		//6:关闭
		rs.close();
		pstmt.close();
		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 2010-09-13 18:10  云飞龙行  阅读(3231)  评论(6编辑  收藏  举报