设计模式解密(12)- 桥接模式
1、简介
定义:将抽象部分与实现部分分离,使它们都可以独立的变化。
主要解决:在多维可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
英文:bridge
类型:结构型
2、问题引入
在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?
先来试想一个情景(OA系统中的消息处理):
消息类型:普通消息,加急消息,特急消息
消息方式:系统内消息,手机短信,邮件
在不使用桥接模式的情况下(即:使用继承方式),首先我们想到的方案:为每一种消息方式都提供一套消息类型,或者为每一种消息类型都提供一套消息方式;
示意图如下:
或者:
实现代码(按第二个图实现代码逻辑)(这里区分普通,加急,特急消息,采用的是在消息头加上标记,例:*加急*:.....):
package com.designpattern.bridge.solution_1; /** * 消息统一接口 * @author Json */ public interface Message { /** * 发送消息 * @param message 消息内容 * @param users 接收人 */ public void send(String message,String users); }
消息类型(3种):
package com.designpattern.bridge.solution_1; /** * 普通消息接口 * @author Json */ public interface CommonMessage extends Message { }
package com.designpattern.bridge.solution_1; /** * 加急消息接口 * @author Json */ public interface UrgencyMessage extends Message { }
package com.designpattern.bridge.solution_1; /** * 特急消息接口 * @author Json */ public interface VeryUrgencyMessage extends Message { /** * 扩展自己的新功能:特急任务,需要催促接收人尽快完成 * @param messageId 消息的编号 * @return */ public void urge(String messageId); }
普通消息 对应 3种消息发送方式:
package com.designpattern.bridge.solution_1; /** * 使用站内消息方式发送【普通】信息 * @author Json */ public class CommonSystemMessage implements UrgencyMessage { @Override public void send(String message, String users) { message = "*普通*:" + message; System.out.println("使用站内消息方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_1; /** * 使用短信方式发送【普通】信息 * @author Json */ public class CommonMobileMessage implements UrgencyMessage { @Override public void send(String message, String users) { message = "*普通*:" + message; System.out.println("使用短信方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_1; /** * 使用邮件方式发送【普通】信息 * @author Json */ public class CommonEmailMessage implements CommonMessage { @Override public void send(String message, String users) { message = "*普通*:" + message; System.out.println("使用邮件方式,发送信息【"+message+"】To【"+users+"】"); } }
紧急消息 对应 3种消息发送方式(代码类似,这里折叠了):
package com.designpattern.bridge.solution_1; /** * 使用站内消息方式发送【加急】信息 * @author Json */ public class UrgencySystemMessage implements UrgencyMessage { @Override public void send(String message, String users) { message = "*加急*:" + message; System.out.println("使用站内消息方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_1; /** * 使用短信方式发送【加急】信息 * @author Json */ public class UrgencyMobileMessage implements UrgencyMessage { @Override public void send(String message, String users) { message = "*加急*:" + message; System.out.println("使用短信方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_1; /** * 使用邮件方式发送【加急】信息 * @author Json */ public class UrgencyEmailMessage implements UrgencyMessage { @Override public void send(String message, String users) { message = "*加急*:" + message; System.out.println("使用邮件方式,发送信息【"+message+"】To【"+users+"】"); } }
紧急消息 对应 3种消息发送方式(代码类似,这里折叠了):
package com.designpattern.bridge.solution_1; /** * 使用站内消息方式发送【特急】信息 * @author Json */ public class VeryUrgencySystemMessage implements VeryUrgencyMessage { @Override public void send(String message, String users) { message = "*特急*:" + message; System.out.println("使用站内消息方式,发送信息【"+message+"】To【"+users+"】"); } /** * 扩展自己的新功能:特急任务,需要催促接收人尽快完成 * @param messageId 消息的编号 * @return */ @Override public void urge(String messageId) { //发出催促的信息 ,比如:每隔半小时 发送一条催促消息 //TODO 逻辑 } }
package com.designpattern.bridge.solution_1; /** * 使用短信方式发送【特急】信息 * @author Json */ public class VeryUrgencyMobileMessage implements VeryUrgencyMessage { @Override public void send(String message, String users) { message = "*特急*:" + message; System.out.println("使用短信方式,发送信息【"+message+"】To【"+users+"】"); } /** * 扩展自己的新功能:特急任务,需要催促接收人尽快完成 * @param messageId 消息的编号 * @return */ @Override public void urge(String messageId) { //发出催促的信息 ,比如:每隔半小时 发送一条催促消息 //TODO 逻辑 } }
package com.designpattern.bridge.solution_1; /** * 使用邮件方式发送【特急】信息 * @author Json */ public class VeryUrgencyEmailMessage implements VeryUrgencyMessage { @Override public void send(String message, String users) { message = "*特急*:" + message; System.out.println("使用邮件方式,发送信息【"+message+"】To【"+users+"】"); } /** * 扩展自己的新功能:特急任务,需要催促接收人尽快完成 * @param messageId 消息的编号 * @return */ @Override public void urge(String messageId) { //发出催促的信息 ,比如:每隔半小时 发送一条催促消息 //TODO 逻辑 } }
测试代码:
package com.designpattern.bridge.solution_1; /** * 测试 * @author Json */ public class Client { public static void main(String[] args) { Message s; //创建普通消息 s = new CommonEmailMessage(); s.send("本月需完成任务如下:...", "Json_Wang"); //创建一个加急消息对象 s = new UrgencyEmailMessage(); s.send("本周需完成任务如下:...", "Json_Wang"); //创建一个特急消息对象 s = new VeryUrgencyEmailMessage(); s.send("尽快修复致命bug,今天客户端无法登陆的问题!", "Json_Wang"); System.out.println("---------------------------------"); //创建普通消息 s = new CommonMobileMessage(); s.send("本月需完成任务如下:...", "Json_Wang"); //创建一个加急消息对象 s = new UrgencyMobileMessage(); s.send("本周需完成任务如下:...", "Json_Wang"); //创建一个特急消息对象 s = new VeryUrgencyMobileMessage(); s.send("尽快修复致命bug,今天客户端无法登陆的问题!", "Json_Wang"); //站内消息实现省略... } }
结果:
使用邮件方式,发送信息【*普通*:本月需完成任务如下:...】To【Json_Wang】
使用邮件方式,发送信息【*加急*:本周需完成任务如下:...】To【Json_Wang】
使用邮件方式,发送信息【*特急*:尽快修复致命bug,今天客户端无法登陆的问题!】To【Json_Wang】
---------------------------------
使用短信方式,发送信息【*普通*:本月需完成任务如下:...】To【Json_Wang】
使用短信方式,发送信息【*加急*:本周需完成任务如下:...】To【Json_Wang】
使用短信方式,发送信息【*特急*:尽快修复致命bug,今天客户端无法登陆的问题!】To【Json_Wang】
上面这样实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
试想一下:新增一种消息类型或者消息方式,我们就要实现其对应的全套实现;例如,增加一种消息类型,需要实现所有不同的消息发送方式;
这意味着,以后每次扩展一下消息类型或消息方式,都必须要实现这其对应的全套处理方式,是不是很痛苦?
采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的类型或方式不太容易,很容易会造成类爆炸问题,扩展起来不灵活。
就没有更好的解决方案吗?
3、解决问题 && 桥接模式类图及组成
先来看看桥接模式的类图及组成,解决方案及代码稍后讲解;
(引)类图:
组成:
抽象类(Abstraction):定义抽象类的接口,维护一个指向Implementor类型对象的引用。
扩充抽象类(RefinedAbstraction):扩充由Abstraction定义的接口。
实现类接口(Implementor):定义实现类的接口,该接口不一定要与Abstraction的接口完全一致;事实上这两个接口可以完全不同。一般来讲, Implementor接口仅提供基本操作,而 Abstraction则定义了基于这些基本操作的较高层次的操作。
具体实现类(ConcreteImplementor):实现Implementor接口并定义它的具体实现。
代码结构:
/** * 定义实现部分的接口,可以与抽象部分接口的方法不一样 */ public interface Implementor { /** * 示例方法,实现抽象部分需要的某些具体功能 */ public void operationImpl(); } /** * 定义抽象部分的接口 */ 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定义的接口功能 */ public class RefinedAbstraction extends Abstraction { public RefinedAbstraction(Implementor impl) { super(impl); } /** * 示例操作,实现一定的功能 */ public void otherOperation(){ //实现一定的功能,可能会使用具体实现部分的实现方法, //但是本方法更大的可能是使用Abstraction中定义的方法, //通过组合使用Abstraction中定义的方法来完成更多的功能 } }
这里大家对桥接模式有点印象没?
接下来,咱们利用桥接模式来解决上述的问题:解决方案:是根据实际需要对消息类型和消息方式进行组合。
示意图如下:
下面在维度上看一下组合情况:
实现代码:
package com.designpattern.bridge.solution_2; /** * 实现发送消息的统一接口 * @author Json */ public interface MessageImplementor { /** * 发送消息 * @param message 消息内容 * @param toUser 接收人 */ public void send(String message,String users); }
package com.designpattern.bridge.solution_2; /** * 抽象的消息对象 * @author Json */ 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); } }
3种消息方式:
package com.designpattern.bridge.solution_2; /** * 使用站內消息方式发送信息 * @author Json */ public class SystemMessage implements MessageImplementor { @Override public void send(String message, String users) { System.out.println("使用站內消息方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_2; /** * 使用短信方式发送信息 * @author Json */ public class MobileMessage implements MessageImplementor { @Override public void send(String message, String users) { System.out.println("使用短信方式,发送信息【"+message+"】To【"+users+"】"); } }
package com.designpattern.bridge.solution_2; /** * 使用邮件方式发送信息 * @author Json */ public class EmailMessage implements MessageImplementor { @Override public void send(String message, String users) { System.out.println("使用邮件方式,发送信息【"+message+"】To【"+users+"】"); } }
3种消息类型:
package com.designpattern.bridge.solution_2; /** * 扩展抽象的消息 -- 普通消息 * @author Json */ public class CommonMessage extends AbstractMessage{ public CommonMessage(MessageImplementor impl) { super(impl); } public void sendMessage(String message, String toUser) { message = "*普通*:" + message; super.sendMessage(message, toUser); } }
package com.designpattern.bridge.solution_2; /** * 扩展抽象的消息 -- 加急消息 * @author Json */ public class UrgencyMessage extends AbstractMessage{ public UrgencyMessage(MessageImplementor impl) { super(impl); } public void sendMessage(String message, String toUser) { message = "*加急*:" + message; super.sendMessage(message, toUser); } }
package com.designpattern.bridge.solution_2; /** * 扩展抽象的消息 -- 特急消息 * @author Json */ public class VeryUrgencyMessage extends AbstractMessage{ public VeryUrgencyMessage(MessageImplementor impl) { super(impl); } public void sendMessage(String message, String toUser) { message = "*特急*:" + message; super.sendMessage(message, toUser); } /** * 扩展自己的新功能:特急任务,需要催促接收人尽快完成 * @param messageId 消息的编号 * @return */ public void urge(String messageId) { //发出催促的信息 ,比如:每隔半小时 发送一条催促消息 //TODO 逻辑 } }
测试:
package com.designpattern.bridge.solution_2; /** * 测试 * @author Json */ public class Client { public static void main(String[] args) { //创建具体的实现对象 MessageImplementor impl = new EmailMessage(); //创建一个普通消息对象 AbstractMessage m = new CommonMessage(impl); m.sendMessage("本月需完成任务如下:...", "Json_Wang"); //创建一个加急消息对象 m = new UrgencyMessage(impl); m.sendMessage("本周需完成任务如下:...", "Json_Wang"); //创建一个特急消息对象 m = new VeryUrgencyMessage(impl); m.sendMessage("尽快修复致命bug,今天客户端无法登陆的问题!", "Json_Wang"); System.out.println("--------------------------------------"); //把实现方式切换成手机短消息,然后再实现一遍 impl = new MobileMessage(); m = new CommonMessage(impl); m.sendMessage("本月需完成任务如下:...", "Json_Wang"); m = new UrgencyMessage(impl); m.sendMessage("本周需完成任务如下:...", "Json_Wang"); m = new VeryUrgencyMessage(impl); m.sendMessage("尽快修复致命bug,今天客户端无法登陆的问题!", "Json_Wang"); //站内消息实现省略... } }
结果:
使用邮件方式,发送信息【*普通*:本月需完成任务如下:...】To【Json_Wang】
使用邮件方式,发送信息【*加急*:本周需完成任务如下:...】To【Json_Wang】
使用邮件方式,发送信息【*特急*:尽快修复致命bug,今天客户端无法登陆的问题!】To【Json_Wang】
--------------------------------------
使用短信方式,发送信息【*普通*:本月需完成任务如下:...】To【Json_Wang】
使用短信方式,发送信息【*加急*:本周需完成任务如下:...】To【Json_Wang】
使用短信方式,发送信息【*特急*:尽快修复致命bug,今天客户端无法登陆的问题!】To【Json_Wang】
趁热打铁,下面介绍一下桥接模式的优缺点;
4、优缺点
优点:
1、 抽象和实现的分离:让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统,从而产生更好的结构化系统,系统的高层部分仅需知道Abstraction和Implementor即可。
2、 优秀的扩展能力:由于桥接模式把抽象和实现部分分离开了,而且分别定义接口,这就使得抽象部分和实现部分可以分别独立的扩展,而不会相互影响,从而大大的提高了系统的可扩展性。 可动态切换实现。由于桥接模式把抽象和实现部分分离开了,那么在实现桥接的时候,就可以实现动态的选择和使用具体的实现,也就是说一个实现不再是固定的绑定在一个抽象接口上了,可以实现运行期间动态的切换实现。你可以独立地对Abstraction和Implementor层次结构进行扩充。
3、可减少子类的个数:根据前面的讲述,对于有两个变化纬度的情况,如果采用继承的实现方式,大约需要两个纬度上的可变化数量的乘积个子类;而采用桥接模式来实现的话,大约需要两个纬度上的可变化数量的和个子类。可以明显地减少子类的个数
。
3、 实现细节对客户透明: 你可以对客户隐藏实现细节,例如共享 Implementor对象以及相应的引用计数机制(如果有的话) 。
缺点:
桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
5、应用场景
1、如果你不希望在抽象和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象和实现部分分开,然后在程序运行期间来动态的设置抽象部分需要用到的具体的实现,还可以动态切换具体的实现。
2、如果出现抽象部分和实现部分都应该可以扩展的情况,可以采用桥接模式,让抽象部分和实现部分可以独立的变化,从而可以灵活的进行单独扩展,而不是搅在一起,扩展一边会影响到另一边。
3、如果希望实现部分的修改,不会对客户产生影响,可以采用桥接模式,客户是面向抽象的接口在运行,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,也可以说对客户是透明的。
4、如果采用继承的实现方案,会导致产生很多子类,对于这种情况,可以考虑采用桥接模式,分析功能变化的原因,看看是否能分离成不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目。
实际遇到过的应用:
非常典型的例子 -- JDBC驱动程序
人力资源系统中的奖金计算模块:
奖金分类:个人奖金,团体奖金,项目奖金,激励奖金
部门分类:人事部,销售部,研发部
OA系统中的消息处理:
业务类型:普通消息,加急消息,特急消息
发送消息方式:系统内消息,手机短信,邮件
JDBC示例代码:
package com.designpattern.bridge; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * JDBC示例讲解 桥接模式 * @author Json */ public class scene_example { public static void main(String[] args) throws SQLException, ClassNotFoundException { 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 id = rs.getString("id"); } //6:关闭 rs.close(); pstmt.close(); conn.close(); } }
解析:
我们写的应用程序,是面向JDBC的API在开发,这些接口就相当于桥接模式中的抽象部分的接口。
这些API是通过DriverManager来得到的。
JDBC的驱动程序实现了JDBC的API,驱动程序就相当于桥接模式中的具体实现部分(不同数据库对于JDBC驱动的实现也是不一样的,也就是不同的数据库会有不同的驱动实现)。
抽象部分——JDBC的API,具体实现部分——驱动程序,通过DriverManager来获取相应的对象。
JDBC的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。对于应用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而无需更改应用程序,从而实现在不同的数据库上移植;对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序。
6、桥接模式与继承
继承是扩展对象功能的一种常见手段,通常情况下,继承扩展的功能变化纬度都是一纬的,也就是变化的因素只有一类。
对于出现变化因素有两类的,也就是有两个变化纬度的情况,继承实现就会比较痛苦。比如上面的示例,就有两个变化纬度,一个是消息类型;另外一个是消息方式。
从理论上来说,如果用继承的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的乘积那么多个。比如上面的示例,在消息类型的纬度上,目前的可变数量是3个,普通消息、加急消息和特急消息;在消息发送方式的纬度上,目前的可变数量也是3个,站内消息、Email和短信。这种情况下,如果要实现全的话,那么需要的实现类应该是:3 X 3 = 9个。
如果要在任何一个纬度上进行扩展,都需要实现另外一个纬度上的可变数量那么多个实现类,这也是为何会感到扩展起来很困难。而且随着程序规模的加大,会越来越难以扩展和维护。
而桥接模式就是用来解决这种有两个变化纬度的情况下,如何灵活的扩展功能的一个很好的方案。其实,桥接模式主要是把继承改成了使用对象组合,从而把两个纬度分开,让每一个纬度单独去变化,最后通过对象组合的方式,把两个纬度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效的减少了实际实现的类的个数。
从理论上来说,如果用桥接模式的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的和那么多个。同样是上面那个示例,使用桥接模式来实现,实现全的话,最后需要的实现类的数目应该是:3 + 3 = 6个。
这也从侧面体现了,使用对象组合的方式比继承要来得更灵活。
7、桥接模式与策略模式
桥接模式:不仅Implementor具有变化(ConcreteImplementor),而且Abstraction也可以发生变化(RefinedAbstraction),而且两者的变化是完全独立的,RefinedAbstraction与ConcreateImplementor之间松散耦合,它们仅仅通过Abstraction与Implementor之间的关系联系起来。强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作。
策略模式:并不考虑Context的变化,只有算法的可替代性。强调Strategy抽象接口的提供的是一种算法,一般是无状态、无数据的,Context简单调用这些算法完成其操作。
目的不一样,策略模式的目的是封装一系列的算法,使得这些算法可以相互替换,调用算法的Context类不会发生变化;而桥接模式的目的是分离抽象和实现部分,使得它们可以独立的变化。
严格来讲,策略模式其实是包含在桥接模式中的。
8、总结
1、桥接模式实现了抽象化与实现化的脱耦。他们两个互相独立,不会影响到对方。
2、对于两个独立变化的维度,使用桥接模式再适合不过了。
PS:源码地址 https://github.com/JsonShare/DesignPattern/tree/master
PS:原文地址 http://www.cnblogs.com/JsonShare/p/7233342.html