1.模式定义:
桥接模式是对象的结构模式。又称为柄体(Handle and Body)模式或接口(Interface)模式。桥接模式的用意是“将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化”。
2.模式特点:
桥接模式虽然不是一个使用频率很高的模式,但是熟悉这个模式对于理解面向对象的设计原则,包括“开-闭”原则以及组合/聚合复用原则都很有帮助。理解好这两个原则,有助于形成正确的设计思想和培养良好的设计风格。
桥接模式的用意是“将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化”。这句话很短,但是第一次读到这句话的人很可能都会思考良久而不解其意。
这句话有三个关键词,也就是抽象化、实现化和脱耦。理解这三个词所代表的概念是理解桥接模式用意的关键:
(1)抽象化
从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征,就是抽象化。例如苹果、香蕉、生梨、 桃子等,它们共同的特性就是水果。得出水果概念的过程,就是一个抽象化的过程。要抽象,就必须进行比较,没有比较就无法找到在本质上共同的部分。共同特征是指那些能把一类事物与他类事物区分开来的特征,这些具有区分作用的特征又称本质特征。因此抽取事物的共同特征就是抽取事物的本质特征,舍弃非本质的特征。 所以抽象化的过程也是一个裁剪的过程。在抽象时,同与不同,决定于从什么角度上来抽象。抽象的角度取决于分析问题的目的。
通常情况下,一组对象如果具有相同的特征,那么它们就可以通过一个共同的类来描述。如果一些类具有相同的特征,往往可以通过一个共同的抽象类来描述。
(2)实现化
抽象化给出的具体实现,就是实现化。
一个类的实例就是这个类的实例化,一个具体子类是它的抽象超类的实例化。
(3)脱耦
所谓耦合,就是两个实体的行为的某种强关联。而将它们的强关联去掉,就是耦合的解脱,或称脱耦。在这里,脱耦是指将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联。
所谓强关联,就是在编译时期已经确定的,无法在运行时期动态改变的关联;所谓弱关联,就是可以动态地确定并且可以在运行时期动态地改变的关联。显然,在Java语言中,继承关系是强关联,而聚合关系是弱关联。
将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改换成为弱关联。因此,桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用聚合关系而不是继承关系,从而使两者可以相对独立地变化。这就是桥接模式的用意。
- 桥接模式使用对象见的组合关系解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。
- 所谓抽象和实现沿着各自维度的变化,即“子类化”它们,得到各个子类之后,便可以任意它们,从而获得不同路上的不同其次。
- 桥接模式有时候类似于多继承方案,但是多继承方案往往违背了SRP原则,复用性较差。桥接模式是比继承方案更好的解决方法。
- 桥接模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换而言之两个变化不会导致纵横交错的结果,并不一定要使用桥接模式。
3.使用场景:
(1)不希望在抽象和它的实现部分之间有一个固定的绑定关系
(2)抽象部分以及实现部分都想通过子类生成一定的扩充内容
(3)对一个抽象的实现部分的修改对客户不产生影响
(4)如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
(5)抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
(6)一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
(7)虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
(8)对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
4.模式实现:
可以看出,这个系统含有两个等级结构:
一、由抽象化角色和修正抽象化角色组成的抽象化等级结构。
二、由实现化角色和两个具体实现化角色所组成的实现化等级结构。
桥接模式所涉及的角色有:
(1)抽象化(Abstraction)角色:
抽象化给出的定义,并保存一个对实现化对象的引用。
public abstract class Abstraction { protected Implementor impl; public Abstraction(Implementor impl){ this.impl = impl; } //示例方法 public void operation(){ impl.operationImpl(); } }
(2)修正抽象化(RefinedAbstraction)角色:
扩展抽象化角色,改变和修正父类对抽象化的定义。
public class RefinedAbstraction extends Abstraction { public RefinedAbstraction(Implementor impl) { super(impl); } //其他的操作方法 public void otherOperation(){ } }
(3)实现化(Implementor)角色:
这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
public abstract class Implementor { /** * 示例方法,实现抽象部分需要的某些具体功能 */ public abstract void operationImpl(); }
(4)具体实现化(ConcreteImplementor)角色:
这个角色给出实现化角色接口的具体实现。
public class ConcreteImplementorA extends Implementor { @Override public void operationImpl() { //具体操作 } } public class ConcreteImplementorB extends Implementor { @Override public void operationImpl() { //具体操作 } }
抽象化角色就像是一个水杯的手柄,而实现化角色和具体实现化角色就像是水杯的杯身。手柄控制杯身,这就是此模式别名“柄体”的来源。
对象是对行为的封装,而行为是由方法实现的。在这个示意性系统里,抽象化等级结构中的类封装了operation()方法;而实现化等级结构中的类封装的是operationImpl()方法。当然,在实际的系统中往往会有多于一个的方法。
抽象化等级结构中的方法通过向对应的实现化对象的委派实现自己的功能,这意味着抽象化角色可以通过向不同的实现化对象委派,来达到动态地转换自己的功能的目的。
5.优缺点:
(1)桥接模式的优点
[1]实现了抽象和实现部分的分离
桥接模式分离了抽象部分和实现部分,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,分别定义接口,这有助于系统进行分层设计,从而产生更好的结构化系统。对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了。
[2]更好的可扩展性
由于桥接模式把抽象部分和实现部分分离了,从而分别定义接口,这就使得抽象部分和实现部分可以分别独立扩展,而不会相互影响,大大的提供了系统的可扩展性。
[3]可动态的切换实现
由于桥接模式实现了抽象和实现的分离,所以在实现桥接模式时,就可以实现动态的选择和使用具体的实现。
[4]实现细节对客户端透明,可以对用户隐藏实现细节。
(2)桥接模式的缺点
[1]]桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
[2]桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
6.注意事项
Jdk中的桥接模式:JDBC
JDBC连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不动,原因就是JDBC提供了统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了
7.应用实例:
考虑这样一个实际的业务功能:发送提示消息。基本上所有带业务流程处理的系统都会有这样的功能,比如OA上有尚未处理完毕的文件,需要发送一条消息提示他。
从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加加急,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促;从发送消息的手段上看,又有系统内短消息、手机短信息、邮件等。
(1)不使用桥接模式:
实现发送普通消息
先考虑实现一个简单点的版本,比如,消息只是实现发送普通消息,发送的方式只实现系统内短消息和邮件。其他的功能,等这个版本完成后,再继续添加。
代码:
[1]消息的统一接口
public interface Message { /** * 发送消息 * @param message 要发送消息的内容 * @param toUser 消息的接受者 */ public void send(String message , String toUser); }
[2]系统内短消息示例类
public class CommonMessageSMS implements Message { @Override public void send(String message, String toUser) { System.out.println("使用系统内短消息的方法,发送消息'"+message+"'给"+toUser); } }
[3]邮件消息示例类
public class CommonMessageEmail implements Message{ @Override public void send(String message, String toUser) { System.out.println("使用邮件短消息的方法,发送消息'"+message+"'给"+toUser); } }
实现发送加急消息
发送加急消息同样有两种方式,系统内短消息和邮件方式。但是加急消息的实现不同于普通消息,加急消息会自动在消息上添加加急,然后在再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息的处理进度。比如,相应的人员是否接收到这个信息,相应的处理工作是否已经展开。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控功能。
代码:
[1]加急消息的接口
public interface UrgencyMessage extends Message { /** * 监控指定消息的处理过程 * @param messageId 被监控的消息编号 * @return 监控到的消息的处理状态 */ public Object watch(String messageId); }
[2]系统内加急短消息示例类
public class UrgencyMessageSMS implements UrgencyMessage { @Override public Object watch(String messageId) { // 根据消息id获取消息的状态,组织成监控的数据对象,然后返回 return null; } @Override public void send(String message, String toUser) { message = "加急:" + message; System.out.println("使用系统内短消息的方法,发送消息'"+message+"'给"+toUser); } }
[3]邮件加急短消息示例类
public class UrgencyMessageEmail implements UrgencyMessage { @Override public Object watch(String messageId) { // 根据消息id获取消息的状态,组织成监控的数据对象,然后返回 return null; } @Override public void send(String message, String toUser) { message = "加急:" + message; System.out.println("使用邮件短消息的方法,发送消息'"+message+"'给"+toUser); } }
实现发送特急消息
特急消息不需要查看处理进程,只有没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。
观察上面的系统结构图,会发现一个很明显的问题,那就是通过这种继承的方式来扩展消息处理,会非常不方便。实现加急消息处理的时候,必须实现系统内短消息和邮件两种处理方式,因为业务处理可能不同,在实现特急消息处理的时候,又必须实现系统内短信息和邮件两种处理方式。这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,这还不算完,如果要添加新的实现方式呢?
添加发送手机消息的处理方式
如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现中,都添加发送手机消息的处理的。也就是说,发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。
采用通过继承来扩展的实现方式,有个明显的缺点,扩展消息的种类不太容易。不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每一种类的消息,需要实现所有不同的消息发送方式。更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类都有加入这种新的发送方式的实现。
那么究竟该如何才能既实现功能,又可以灵活地扩展呢?
使用桥接模式来解决问题
根据业务的功能要求,业务的变化具有两个维度,一个维度是抽象的消息,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个维度是在具体的消息发送方式上,包括系统内短消息、邮件和手机短消息,这几个方式是平等的,可被切换的方式。
现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了一个纬度的变化会引起另一个纬度进行相应的变化,从而使得程序扩展起来非常困难。
要想解决这个问题,就必须把这两个纬度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。按照桥接模式的结构,给抽象部分和实现部分分别定义接口,然后分别实现它们就可以了。
[1]抽象消息类
public abstract class AbstractMessage { //持有一个实现部分的对象 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); } }
[2]普通消息类
public class CommonMessage extends AbstractMessage { public CommonMessage(MessageImplementor impl) { super(impl); } @Override public void sendMessage(String message, String toUser) { // 对于普通消息,直接调用父类方法,发送消息即可 super.sendMessage(message, toUser); } }
[3]加急消息类
public class UrgencyMessage extends AbstractMessage { public UrgencyMessage(MessageImplementor impl) { super(impl); } @Override public void sendMessage(String message, String toUser) { message = "加急:" + message; super.sendMessage(message, toUser); } /** * 扩展自己的新功能,监控某消息的处理状态 * @param messageId 被监控的消息编号 * @return 监控到的消息的处理状态 */ public Object watch(String messageId) { // 根据消息id获取消息的状态,组织成监控的数据对象,然后返回 return null; } }
[4]实现发送消息的统一接口
public interface MessageImplementor { /** * 发送消息 * @param message 要发送消息的内容 * @param toUser 消息的接受者 */ public void send(String message , String toUser); }
[5]系统内短消息的实现类
public class MessageSMS implements MessageImplementor { @Override public void send(String message, String toUser) { System.out.println("使用系统内短消息的方法,发送消息'"+message+"'给"+toUser); } }
[6]邮件短消息的实现类
public class MessageEmail implements MessageImplementor { @Override public void send(String message, String toUser) { System.out.println("使用邮件短消息的方法,发送消息'"+message+"'给"+toUser); } }
[7]客户端类
public class Client { public static void main(String[] args) { //创建具体的实现对象 MessageImplementor impl = new MessageSMS(); //创建普通消息对象 AbstractMessage message = new CommonMessage(impl); message.sendMessage("加班申请速批","李总"); //将实现方式切换成邮件,再次发送 impl = new MessageEmail(); //创建加急消息对象 message = new UrgencyMessage(impl); message.sendMessage("加班申请速批","李总"); } }
观察上面的例子会发现,采用桥接模式来实现,抽象部分和实现部分分离开了,可以相互独立的变化,而不会相互影响。因此在抽象部分添加新的消息处理(特急消息),对发送消息的实现部分是没有影响的;反过来增加发送消息的方式(手机短消息),对消息处理部分也是没有影响的。