zl程序教程

您现在的位置是:首页 >  其他

当前栏目

设计模式---桥接模式

2023-03-14 22:36:52 时间

桥接模式


业务场景

考虑这样一个实际的业务功能:发送提示消息。基本上所有带业务流程处理的系统都会有这样的功能,比如某人有新的工作了,需要发送一条消息提示他。

从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加加急,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促。从发送消息的手段上看,又有系统内短消息、手机短消息、邮件等等。

现在要实现这样的发送提示消息的功能,该如何实现呢?


1.0简化版本

先考虑实现一个简单点的版本,比如:消息先只是实现发送普通消息,发送的方式呢,先实现系统内短消息和邮件。其它的功能,等这个版本完成过后,再继续添加,这样先把问题简单化,实现起来会容易一点。

(1)由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如图所示:


1.0版本代码实现

消息的统一接口

/**
 * 消息的统一接口
 */
public interface Message
{
    /**
     * 发送消息
     * @param message 要发送的消息内容
     * @param toUser 消息发送的目的人员
     */
    public void send(String message,String toUser);
}

两种实现方式

/**
 * 以Email的方式发送普通消息
 */
public class CommonMessageEmail implements Message{
    public void send(String message, String toUser) {
        System.out.println("使用Email的方式,发送消息'"
                +message+"'给"+toUser);
    }
}


/**
 * 以站内短消息的方式发送普通消息
 */
public  class CommonMessageSMS implements Message{
    public void send(String message, String toUser) {
        System.out.println("使用站内短消息的方式,发送消息'"
                +message+"'给"+toUser);
    }
}

测试:

public class test
{
    @Test
    public void test()
    {
        //测试
        Message message=new CommonMessageEmail();
        message.send("嘿嘿","小朋友");
        message=new CommonMessageSMS();
        message.send("see you again","ly");
    }
}

2.0版本:实现发送加急消息

上面的实现,看起来很简单,对不对。接下来,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。

加急消息的实现跟普通消息不同,加急消息会自动在消息上添加加急,然后再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息处理的进度,比如:相应的人员是否接收到这个信息,相应的工作是否已经开展等等。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能,这个时候,系统的结构如图所示:


2.0版本代码实现

加急消息的接口

/**
 * 加急消息的抽象接口
 */
public interface UrgencyMessage extends Message{
    /**
     * 监控某消息的处理过程
     * @param messageId 被监控的消息的编号
     * @return 包含监控到的数据对象,这里示意一下,所以用了Object
     */
    public Object watch(String messageId);
}

发送站内短消息和Email两种实现方式

public class UrgencyMessageSMS implements UrgencyMessage{
    public void send(String message, String toUser) {
        message = "加急:"+message;
        System.out.println("使用站内短消息的方式,发送消息'"
+message+"'给"+toUser);
    }
 
    public Object watch(String messageId) {
        //获取相应的数据,组织成监控的数据对象,然后返回       
        return null;
    }   
}

public class UrgencyMessageEmail implements UrgencyMessage{
    public void send(String message, String toUser) {
        message = "加急:"+message;
        System.out.println("使用Email的方式,发送消息'"
+message+"'给"+toUser);
    }
    public Object watch(String messageId) {
        //获取相应的数据,组织成监控的数据对象,然后返回       
        return null;
    }   
}

事实上,在实现加急消息发送的功能上,可能会使用前面发送不同消息的功能,也就是让实现加急消息处理的对象继承普通消息的相应实现,这里为了让结构简单一点,清晰一点,所以没有这样做


问题分析

如果继续添加特急消息的处理

特急消息不需要查看处理进程,只要没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。而特急消息、还有催促的发送方式,相应的实现方式还是发送站内短消息和Email两种,此时系统的结构如图所示:

仔细观察上面的系统结构示意图,会发现一个很明显的问题,那就是:通过这种继承的方式来扩展消息处理,会非常不方便

实现加急消息处理的时候,必须实现站内短消息和Email两种处理方式,因为业务处理可能不同;在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式

这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦,这还不算完,如果要添加新的实现方式呢?继续向下看吧


2:继续添加发送手机消息的处理方式

仔细观察现在的实现,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现里面,都要添加发送手机消息的处理的。也就是说:发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。这就意味着,需要添加三个实现


问题总结

采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易,不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。

更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类,都要加入这种新的发送方式的实现。

要是考虑业务功能上再扩展一下呢?比如:要求实现群发消息,也就是一次可以发送多条消息,这就意味着很多地方都得修改


桥接模式

用来解决上述问题的一个合理的解决方案,就是使用桥接模式。那么什么是桥接模式呢?


介绍

桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。


模式结构


角色

• Abstraction:抽象部分的接口。通常在这个对象里面,要维护一个实现部分的对象引用,在抽象对象里面的方法,需要调用实现部分的对象来完成。这个对象里面的方法,通常都是跟具体的业务相关的方法。

• RefinedAbstraction: 扩展抽象部分的接口,通常在这些对象里面,定义跟实际业务相关的方法,这些方法的实现通常会使用Abstraction中定义的方法,也可能需要调用实现部分的对象来完成。

• Implementor:定义实现部分的接口,这个接口不用和Abstraction里面的方法一致,通常是由Implementor接口提供基本的操作,而Abstraction里面定义的是基于这些基本操作的业务方法,也就是说Abstraction定义了基于这些基本操作的较高层次的操作。

• ConcreteImplementor: 真正实现Implementor接口的对象。


理解抽象化与实现化脱耦的概念

  • 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。
  • 实现化:针对抽象化给出的具体实现,就是实现化,抽象化与实现化是一对互逆的概念,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。
  • 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。

桥接模式示例代码

Implementor接口的定义:

/**
 * 定义实现部分的接口,可以与抽象部分接口的方法不一样
 */
public interface Implementor {
    /**
     * 示例方法,实现抽象部分需要的某些具体功能
     */
    public void operationImpl();
}

Abstraction接口的定义,注意一点,虽然说是接口定义,但其实是实现成为抽象类:

/**
 * 定义抽象部分的接口
 */
public abstract class Abstraction {
    /**
     * 持有一个实现部分的对象
     */
    protected Implementor impl;
    /**
     * 构造方法,传入实现部分的对象 
     * @param impl 实现部分的对象
     */
    public Abstraction(Implementor impl){
        this.impl = impl;
    }
    /**
     * 示例操作,实现一定的功能,可能需要转调实现部分的具体实现方法
     */
    public void operation() {
        impl.operationImpl();
    }

具体的实现:

/**
 * 真正的具体实现对象
 */
public class ConcreteImplementorA implements Implementor {
    public void operationImpl() { 
        //真正的实现
    }
}


/**
 * 真正的具体实现对象
 */
public class ConcreteImplementorB implements Implementor {
    public void operationImpl() { 
        //真正的实现
    }
}

扩展Abstraction接口的对象实现:

/**
 * 扩充由Abstraction定义的接口功能
 */
public class RefinedAbstraction extends Abstraction {
    public RefinedAbstraction(Implementor impl) {
        super(impl);
    }
    /**
     * 示例操作,实现一定的功能
     */
    public void otherOperation(){
        //实现一定的功能,可能会使用具体实现部分的实现方法,
        //但是本方法更大的可能是使用Abstraction中定义的方法,
        //通过组合使用Abstraction中定义的方法来完成更多的功能
    }
}

应用桥接模式来解决的思路

仔细分析上面的示例,根据示例的功能要求,示例的变化具有两个纬度,一个纬度是抽象的消息这边,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个纬度在具体的消息发送方式上,包括站内短消息、Email和手机短信息,这几个方式是平等的,可被切换的方式。这两个纬度一共可以组合出9种不同的可能性来,它们的关系如下图所示:

现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了,一个纬度的变化,会引起另一个纬度进行相应的变化,从而使得程序扩展起来非常困难。

要想解决这个问题,就必须把这两个纬度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。

桥接模式通过引入实现的接口,把实现部分从系统中分离出去;那么,抽象这边如何使用具体的实现呢?肯定是面向实现的接口来编程了,为了让抽象这边能够很方便的与实现结合起来,把顶层的抽象接口改成抽象类,在里面持有一个具体的实现部分的实例。

这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。


使用桥接模式重写示例

实现部分定义的接口:

/**
 * 实现发送消息的统一接口
 */
public interface MessageImplementor {
    /**
     * 发送消息
     * @param message 要发送的消息内容
     * @param toUser 消息发送的目的人员
     */
    public void send(String message,String toUser);
}

抽象部分定义的接口:

/**
 * 抽象的消息对象
 */
public abstract class AbstractMessage {
    /**
     * 持有一个实现部分的对象
     */
    protected MessageImplementor impl;
    /**
     * 构造方法,传入实现部分的对象 
     * @param impl 实现部分的对象
     */
    public AbstractMessage(MessageImplementor impl){
        this.impl = impl;
    }
    /**
     * 发送消息,转调实现部分的方法
     * @param message 要发送的消息内容
     * @param toUser 消息发送的目的人员
     */
    public void sendMessage(String message,String toUser){
        this.impl.send(message, toUser);
    }   
}

具体的实现发送消息:

/**
 * 以站内短消息的方式发送消息
 */
public  class MessageSMS implements MessageImplementor{
    public void send(String message, String toUser) {
        System.out.println("使用站内短消息的方式,发送消息'"
+message+"'给"+toUser);
    }
}


/**
 * 以Email的方式发送消息
 */
public class MessageEmail implements MessageImplementor{
    public void send(String message, String toUser) {
        System.out.println("使用Email的方式,发送消息'"
                               +message+"'给"+toUser);
    }
}

扩展抽象的消息接口:

//普通消息
public class CommonMessage extends AbstractMessage{
    public CommonMessage(MessageImplementor impl) {
        super(impl);
    }
    public void sendMessage(String message, String toUser) {
        //对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了
        super.sendMessage(message, toUser);
    }   
}

//加急消息
public class UrgencyMessage extends AbstractMessage{
    public UrgencyMessage(MessageImplementor impl) {
        super(impl);
    }
    public void sendMessage(String message, String toUser) {
        message = "加急:"+message;
        super.sendMessage(message, toUser);
    }
    /**
     * 扩展自己的新功能:监控某消息的处理过程
     * @param messageId 被监控的消息的编号
     * @return 包含监控到的数据对象,这里示意一下,所以用了Object
     */
    public Object watch(String messageId) {
        //获取相应的数据,组织成监控的数据对象,然后返回       
        return null;
    }   
}

添加功能

看了上面的实现,发现使用桥接模式来实现也不是很困难啊,关键得看是否能解决前面提出的问题,那就来添加还未实现的功能看看,添加对特急消息的处理,同时添加一个使用手机发送消息的方式。该怎么实现呢?

新的特急消息的处理类

public class SpecialUrgencyMessage extends AbstractMessage{
    public SpecialUrgencyMessage(MessageImplementor impl) {
        super(impl);
    }
    public void hurry(String messageId) {
        //执行催促的业务,发出催促的信息
    }
    public void sendMessage(String message, String toUser) {
        message = "特急:"+message;
        super.sendMessage(message, toUser);
        //还需要增加一条待催促的信息
    }
}

手机短消息的方式发送消息的实现

/**
 * 以手机短消息的方式发送消息
 */
public  class MessageMobile implements MessageImplementor{
    public void send(String message, String toUser) {
        System.out.println("使用手机短消息的方式,发送消息'"
+message+"'给"+toUser);
    }
}

测试一下功能

public class Client {
    public static void main(String[] args) {
        //创建具体的实现对象
        MessageImplementor impl = new MessageSMS();
        //创建一个普通消息对象
        AbstractMessage m = new CommonMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");       
        //创建一个紧急消息对象
        m = new UrgencyMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");       
        //创建一个特急消息对象
        m = new SpecialUrgencyMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");
         
        //把实现方式切换成手机短消息,然后再实现一遍
        impl = new MessageMobile();
        m = new CommonMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");
        m = new UrgencyMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");
        m = new SpecialUrgencyMessage(impl);
        m.sendMessage("请喝一杯茶", "小李");
    }
}

结果:

使用站内短消息的方式,发送消息'请喝一杯茶'给小李
使用站内短消息的方式,发送消息'加急:请喝一杯茶'给小李
使用站内短消息的方式,发送消息'特急:请喝一杯茶'给小李
使用手机短消息的方式,发送消息'请喝一杯茶'给小李
使用手机短消息的方式,发送消息'加急:请喝一杯茶'给小李
使用手机短消息的方式,发送消息'特急:请喝一杯茶'给小李

深入理解桥接模式

什么是桥接

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

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


为何需要桥接

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


如何桥接

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


动态变换功能

由于桥接模式中的抽象部分和实现部分是完全分离的,因此可以在运行时动态组合具体的真实实现,从而达到动态变换功能的目的。

从另外一个角度看,抽象部分和实现部分没有固定的绑定关系了,因此同一个真实实现可以被不同的抽象对象使用,反过来,同一个抽象也可以有多个不同的实现。就像前面示例的那样,比如:站内短消息的实现功能,可以被普通消息、加急消息或是特急消息等不同的消息对象使用;反过来,某个消息具体的发送方式,可以是站内短消息,或者是Email,也可以是手机短消息等具体的发送方式。


退化的桥接模式

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

也就是说,就算不要Implementor接口了,也要保持Abstraction和Implementor是分离的,模式的分离机制仍然是非常有用的。


桥接模式和继承

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

对于出现变化因素有两类的,也就是有两个变化纬度的情况,继承实现就会比较痛苦

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

这也从侧面体现了,使用对象组合的方式比继承要来得更灵活。


谁来桥接

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

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

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

1:由抽象部分的对象自己来创建相应的Implementor的对象

对于这种情况的实现,又分成两种,一种是需要外部传入参数,一种是不需要外部传入参数。

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

/**
 * 抽象的消息对象
 */
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;
    }
}

小结一下:

对于由抽象部分的对象自己来创建相应的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对象去实现。


反射+配置文件+简单工厂+桥接模式的综合案例2

现需要提供大中小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3*5=15支蜡笔,也就是说必须准备15个具体的蜡笔类。而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3+5=8个类就可以实现15支蜡笔的功能。本实例使用桥接模式来模拟毛笔的使用过程。

代码整理

实现接口和相关类:

//实现部分的接口
public interface Pen
{
 //作画 : 颜色,画什么
    public void bePainting(String colorType,String thing);
}

public class BigPen implements Pen{
    @Override
    public void bePainting(String colorType, String thing)
    {
        System.out.println("正在使用大毛笔绘制"+colorType+"的"+thing);
    }
}

public class MiddlePen implements  Pen{
    @Override
    public void bePainting(String colorType, String thing) {
        System.out.println("正在使用中毛笔绘制"+colorType+"的"+thing);
    }
}

public class SmallPen implements Pen{
    @Override
    public void bePainting(String colorType, String thing) {
        System.out.println("正在使用小毛笔绘制"+colorType+"的"+thing);
    }
}

抽象接口和相关类:

//抽象部分的接口
public abstract class Color
{
    protected Pen pen;
    Color(){}
    public Color setPen(Integer penType)
    {
        //通过静态工厂为我们制造pen
        pen=PenFactory.getBean(penType);
        return  this;
    }
    public abstract void draw(String name);
}


public class Red extends Color{
    public Red() {
    }

    @Override
    public void draw(String name)
    {
          super.pen.bePainting("红色",name);
    }
}


public class yellow extends Color{
    yellow(){}
    @Override
    public void draw(String name) {
    super.pen.bePainting("黄色",name);
    }
}

读取配置文件和反射的类

//使用java反射创建具体的颜色和画笔
public class XmlUtil
{
    //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
    public static Object getBean(String args)
    {
        // DocumentBuilderFactory是一个抽象工厂类
        //创建 DOM 解析器的工厂
        DocumentBuilderFactory dFactory=DocumentBuilderFactory.newInstance();
        try {
            //得到 DOM 解析器对象
            DocumentBuilder db = dFactory.newDocumentBuilder();
            //把要解析的 XML 文档转化为输入流,以便 DOM 解析器解析它
            Document document = db.parse(new File("src/main/resources/config.xml"));
           //创建一个存放节点对象的集合
            NodeList nodeList=null;
            nodeList = document.getElementsByTagName("className");
            //根据传入的args参数,来返回不同的实例镀锡
            Node node=null;
            if(args.equals("color"))
            {
                //获取包含类名的文本节点
                node = nodeList.item(0).getFirstChild();
            }
            else if(args.equals("pen"))
            {
                //获取包含类名的文本节点
                node = nodeList.item(1).getFirstChild();
            }
            //获取节点的文本值
            String value = node.getNodeValue();
            //反射创建对象
            return Class.forName("PenBridge." + value).newInstance();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

配置文件

<?xml version="1.0"?>
<config>
    <className>Red</className>
    <className>SmallPen</className>
</config>

测试:

public class test
{
    @Test
    public void test()
    {
       //首先获取对应的Bean实例
        Color color = (Color)XmlUtil.getBean("color");
         Pen p=(Pen)XmlUtil.getBean("pen");
         color.setPen(2).draw("小朋友");
    }
}

案例3

如果需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Linux、Unix等)上播放多种格式的视频文件,常见的视频格式包括MPEG、RMVB、AVI、WMV等。现使用桥接模式设计该播放器。


模式优缺点

优点

  • 分离抽象接口及其实现部分。
  • 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  • 实现细节对客户透明,可以对用户隐藏实现细节。

缺点

  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

模式适用环境

  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合
  • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  • 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

模式应用

典型例子-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来得到的。此时的系统结构如图所示:

那么这些JDBC的API,谁去实现呢?光有接口,没有实现也不行啊。

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

有了抽象部分——JDBC的API,有了具体实现部分——驱动程序,那么它们如何连接起来呢?就是如何桥接呢?

就是前面提到的DriverManager来把它们桥接起来,从某个侧面来看,DriverManager在这里起到了类似于简单工厂的功能,基于JDBC的应用程序需要使用JDBC的API,如何得到呢?就通过DriverManager来获取相应的对象。

那么此时系统的整体结构如图所示:

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

JDBC的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。而且,JDBC的这种架构,还合理的划分了应用程序开发人员和驱动程序开发人员的边界


模式扩展

适配器模式与桥接模式的联用

  • 桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况

参考文献

桥接模式1

桥接模式2