设计模式 -- 桥接模式(Bridge)
写在前面的话:读书破万卷,编码如有神
--------------------------------------------------------------------
主要内容包括:
- 初始桥接模式,包括: 定义、结构和说明、参考实现
- 体会桥接模式,包括: 场景问题、不用模式的解决方案、使用模式的解决方案
- 理解桥接模式,包括: 认识桥接模式、谁来桥接、典型例子-JDBC、广义桥接-Java中无处不在桥接、桥接模式的优缺点
- 思考桥接模式,包括: 桥接模式的本质、对设计原则的体现、何时选用
参考内容:
1、《研磨设计模式》 一书,作者:陈臣、王斌
---------------------------------------------------------------------
1、初始化桥接模式
1.1、定义
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
1.2、结构和说明
说明:
- Abstraction: 抽象部分的接口。通常在这个对象中,要维护一个实现部分的对象引用,在抽象对象里面的方法,需要调用实现部分的对象来完成。
- RefinedAbstraction:扩展抽象部分的接口。通常在这些对象中,定义根实际业务相关的方法,这些方法的实现通常会使用Abstraction中定义的方法。
- Implementor: 定义实现部分的接口。这个接口不用和Abstraction中的方法一致,通常是由Implementor接口提供基本的操作,而Abstraction里面定义的是基于这些基本操作的业务方法,也就是说Abstraction定义了基于这些基本操作的较高层次的操作。
- ConcreteImplementor: 真正实现Implementor接口的对象。
桥接模式通过引入实现的接口,把实现部分从系统中分离出去。那么,抽象这边持有一个具体的实现部分的实例就可以使用具体的实现了。
1.3、参考实现
1 /** 2 * 定义实现部分的接口,可以与抽象部分接口的方法不一样 3 */ 4 public interface Implementor { 5 6 /** 7 * 实现抽象部分需要的某些具体功能 8 */ 9 public void operationImpl(); 10 } 11 12 /** 13 * 定义抽象部分的接口 14 */ 15 public abstract class Abstraction { 16 17 //持有一个实现部分的对象 18 private Implementor impl; 19 20 /** 21 * 构造方法 22 * @param impl 实现部分的对象 23 */ 24 public Abstraction(Implementor impl){ 25 this.impl = impl; 26 } 27 28 /** 29 * 实现一定的功能,可能需要转调实现部分的具体方法 30 */ 31 public void operation(){ 32 this.impl.operationImpl(); 33 } 34 } 35 36 /** 37 * 真正的具体实现对象 38 */ 39 public class ConcreteImplementorA implements Implementor { 40 41 @Override 42 public void operationImpl() { 43 //真正的实现 44 } 45 } 46 47 /** 48 * 真正的具体实现对象 49 */ 50 public class ConcreteImplementorB implements Implementor { 51 52 @Override 53 public void operationImpl() { 54 //真正的实现 55 } 56 } 57 58 /** 59 * 扩充由Abstraction定义的接口功能 60 */ 61 public class RefinedAbstraction extends Abstraction { 62 63 public RefinedAbstraction(Implementor impl) { 64 super(impl); 65 } 66 67 /** 68 * 实现一定的功能 69 */ 70 public void otherOperation(){ 71 72 } 73 }
---------------------------------------------------------------------
2、体会桥接模式
2.1、发送提示消息
考虑这样一个实际功能: 发送提示消息。从业务上看,提示消息分为:普通消息、加急消息、特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加加急,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促; 从发送消息的手段上看,又有系统内短消息、手机短消息、邮件消息等。
2.2、不用模式的解决方案
(1)简化版本
先考虑一个简化版本,消息只发送普通消息,发送方式有:系统内短消息、邮件消息两种。
由于发送普通消息会有两种不同的实现方式,为了让外部统一操作,因此,把消息设计成接口,然后由两个不同的实现类分别实现系统内短消息方式和邮件发送消息的方式。
系统结构如下图:
示例代码:
1 /** 2 * 消息的统一接口 3 */ 4 public interface Message { 5 /** 6 * 发送消息 7 * @param message 要发送的消息内容 8 * @param toUser 消息发送的目的人员 9 */ 10 public void send(String message,String toUser); 11 } 12 13 /** 14 * 以站内短消息的方式发送普通消息 15 */ 16 public class CommonMessageSMS implements Message { 17 18 @Override 19 public void send(String message, String toUser) { 20 System.out.println("使用站内短消息的方式,发送消息: " + message + ",给"+toUser); 21 } 22 } 23 24 /** 25 * 以Email的方式发送普通消息 26 */ 27 public class CommonMessageEmail implements Message{ 28 29 @Override 30 public void send(String message, String toUser) { 31 System.out.println("使用Email消息的方式,发送消息: " + message + ",给"+toUser); 32 } 33 }
(2)实现发送加急消息
接着上面的实现,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。(加急消息会自动在消息上添加加急,然后再发送消息,另外加急消息会提供监控的方法,让客户端可以随时查看加急消息的处理进度。因此加急消息需要扩展一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能。)
系统结构如下图:
示例代码:
1 /** 2 * 加急消息的抽象接口 3 */ 4 public interface UrgencyMessage extends Message { 5 6 /** 7 * 监控某消息的处理过程 8 * @param messageId 被监控的消息的编号 9 * @return 10 */ 11 public Object watch(String messageId); 12 } 13 14 /** 15 * 以站内短消息的方式发送加急消息 16 */ 17 public class UrgencyMessageSMS implements UrgencyMessage { 18 19 @Override 20 public void send(String message, String toUser) { 21 message = "[加急]:" + message; 22 System.out.println("使用站内短消息的方式,发送消息: " + message + ",给"+toUser); 23 } 24 25 @Override 26 public Object watch(String messageId) { 27 //获取相应的数据,组织成监控的数据对象,然后返回 28 return null; 29 } 30 } 31 32 /** 33 * 以站内Email的方式发送加急消息 34 */ 35 public class UrgencyMessageEmail implements UrgencyMessage { 36 37 @Override 38 public void send(String message, String toUser) { 39 message = "[加急]:" + message; 40 System.out.println("使用Email消息的方式,发送消息: " + message + ",给"+toUser); 41 } 42 43 @Override 44 public Object watch(String messageId) { 45 //获取相应的数据,组织成监控的数据对象,然后返回 46 return null; 47 } 48 }
2.3、有何问题
看了上面的实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
下面继续添加新的功能:
(1)添加特急消息的处理(特急需要需要添加一个催促功能)
系统结构如下图:
观察上面的系统结构示意图,会发现一个很明显的问题,那就是通过这种继承的方式来扩展消息处理,会非常不方便。会看到在实现加急消息处理的时候,必须要实现站内短消息和Email两种处理方式;而在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式。
这就意味着,以后每扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦?如果再要添加新的消息发送方式呢?
(2)添加发送手机消息的处理方式
根据目前的实现来看,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现中,都要添加发送手机消息的处理。也就是说,发送普通消息、加急消息、特急消息的处理,都可以通过手机来发送。
系统结构如下图:
把这个问题总结一下:采用通过继承来扩展的实现方式,有个明显的缺点,扩展消息的种类不太容易。不同种类的消息具有不同的业务,在这种情况下每个种类的消息,都需要实现所有不同的消息发送方式。更可怕的是,如果要加入新的消息发送方式或者新的消息类型的话,需要修改的地方太多了。
2.4、使用桥接模式来解决问题
(1)解决思路
分析上面的示例,可以看出示例的变化具有两个维度,一个维度是抽象的消息这边,包括:普通消息、加急消息、特急消息;另一个维度是在具体的消息发送方式上,包括:站内短消息、E-mail消息、手机消息。这两个维度一共可以组合出9种不同的可能性来。
它们的关系如下图:
(ps:现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致一个维度的变化会引起另一个维度进行相应的变化,从而使得程序扩展起来非常困难。)
要解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让他们相互独立,这样就可以实现独立变化了。
(2)示例代码
使用桥接模式来重写实现示例,首要任务就是要把抽象部分和实现部分分离出来,分析要实现的功能。抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。
(a)先从简单的功能开始
消息类型:普通消息、加急消息; 发送方式: 站内短消息、E-mail。
使用桥接模式来实现这些功能的程序结构如下图:
示例代码如下:
1 (1)实现部分定义的接口 2 /** 3 * 实现发送消息的统一接口 4 */ 5 public interface MessageImplementor { 6 7 /** 8 * 发送消息 9 * @param message 要发送的消息内容 10 * @param toUser 消息发送的目的人员 11 */ 12 public void send(String message,String toUser); 13 } 14 (2)抽象部分定义的接口 15 /** 16 * 抽象的消息对象 17 */ 18 public abstract class AbstractMessage { 19 20 /** 21 * 持有一个实现部分的对象 22 */ 23 protected MessageImplementor impl; 24 25 /** 26 * 构造方法 27 * @param impl 28 */ 29 public AbstractMessage(MessageImplementor impl){ 30 this.impl = impl; 31 } 32 33 /** 34 * 发送消息,转调实现部分的功能 35 * @param message 要发送的消息内容 36 * @param toUser 消息发送的目的人员 37 */ 38 public void sendMessage(String message,String toUser){ 39 this.impl.send(message, toUser); 40 } 41 } 42 (3)站内短消息的实现 43 /** 44 * 以站内短消息的方式发送消息 45 */ 46 public class MessageSMS implements MessageImplementor { 47 48 @Override 49 public void send(String message, String toUser) { 50 System.out.println("使用站内短消息的方式,发送消息: " + message + ",给"+toUser); 51 } 52 } 53 54 (4)Email方式的实现 55 /** 56 * 以Email的方式发送消息 57 */ 58 public class MessageEmail implements MessageImplementor { 59 60 @Override 61 public void send(String message, String toUser) { 62 System.out.println("使用Email消息的方式,发送消息: " + message + ",给"+toUser); 63 } 64 } 65 66 (5)普通消息的实现 67 /** 68 * 普通消息的实现 69 */ 70 public class CommonMessage extends AbstractMessage { 71 72 public CommonMessage(MessageImplementor impl) { 73 super(impl); 74 } 75 76 @Override 77 public void sendMessage(String message, String toUser) { 78 //对于普通消息,什么都不干,直接调用父类的方法,把消息发送出去就可以了。 79 super.sendMessage(message, toUser); 80 } 81 } 82 (6)加急消息的实现 83 /** 84 * 加急消息 85 */ 86 public class UrgencyMessage extends AbstractMessage { 87 88 public UrgencyMessage(MessageImplementor impl) { 89 super(impl); 90 } 91 92 @Override 93 public void sendMessage(String message, String toUser) { 94 message = "[加急]:"+message; 95 super.sendMessage(message, toUser); 96 } 97 98 /** 99 * 扩展新功能:监控某消息的处理过程 100 * @param messageId 101 * @return 102 */ 103 public Object watch(String messageId){ 104 return null; 105 } 106 } 107 108 (7)测试客户端 109 public class Client { 110 public static void main(String[] args) { 111 MessageSMS sms = new MessageSMS(); 112 CommonMessage cm = new CommonMessage(sms); 113 UrgencyMessage um = new UrgencyMessage(sms); 114 cm.sendMessage("你有一个回复", "小明"); 115 um.sendMessage("你有一个回复", "小明"); 116 117 MessageEmail email = new MessageEmail(); 118 CommonMessage cm2 = new CommonMessage(email); 119 UrgencyMessage um2 = new UrgencyMessage(email); 120 cm2.sendMessage("你有一个包裹", "小明"); 121 um2.sendMessage("你有一个包裹", "小明"); 122 } 123 } 124 125 运行结果: 126 使用站内短消息的方式,发送消息: 你有一个回复,给小明 127 使用站内短消息的方式,发送消息: [加急]:你有一个回复,给小明 128 使用Email消息的方式,发送消息: 你有一个包裹,给小明 129 使用Email消息的方式,发送消息: [加急]:你有一个包裹,给小明
(b)添加功能,消息类型增加:特急消息;发送方式增加:手机短信。
示例代码如下:
1 /** 2 * 特急消息 3 */ 4 public class SpecialUrgencyMessage extends AbstractMessage { 5 6 public SpecialUrgencyMessage(MessageImplementor impl) { 7 super(impl); 8 } 9 10 @Override 11 public void sendMessage(String message, String toUser) { 12 message = "[特急]:" + message; 13 super.sendMessage(message, toUser); 14 } 15 16 public void hurry(String messageId){ 17 //执行催促业务 18 } 19 } 20 21 /** 22 * 手机短消息的方式发送消息 23 */ 24 public class MessageMobile implements MessageImplementor { 25 26 @Override 27 public void send(String message, String toUser) { 28 System.out.println("使用手机短消息的方式,发送消息:" + message+",给 "+toUser); 29 } 30 } 31 32 33 客户端测试 34 public class Client { 35 public static void main(String[] args) { 36 MessageSMS sms = new MessageSMS(); 37 CommonMessage cm = new CommonMessage(sms); 38 UrgencyMessage um = new UrgencyMessage(sms); 39 cm.sendMessage("你有一个回复", "小明"); 40 um.sendMessage("你有一个回复", "小明"); 41 System.out.println("-------------------------"); 42 MessageEmail email = new MessageEmail(); 43 CommonMessage cm2 = new CommonMessage(email); 44 UrgencyMessage um2 = new UrgencyMessage(email); 45 cm2.sendMessage("你有一个包裹", "小明"); 46 um2.sendMessage("你有一个包裹", "小明"); 47 System.out.println("-------------------------"); 48 MessageMobile mobile = new MessageMobile(); 49 CommonMessage cm3 = new CommonMessage(mobile); 50 UrgencyMessage um3 = new UrgencyMessage(mobile); 51 SpecialUrgencyMessage sum3 = new SpecialUrgencyMessage(mobile); 52 cm3.sendMessage("有人赞了你", "小东"); 53 um3.sendMessage("有人赞了你", "小东"); 54 sum3.sendMessage("有人赞了你", "小东"); 55 } 56 } 57 58 运行结果: 59 使用站内短消息的方式,发送消息: 你有一个回复,给小明 60 使用站内短消息的方式,发送消息: [加急]:你有一个回复,给小明 61 ------------------------- 62 使用Email消息的方式,发送消息: 你有一个包裹,给小明 63 使用Email消息的方式,发送消息: [加急]:你有一个包裹,给小明 64 ------------------------- 65 使用手机短消息的方式,发送消息:有人赞了你,给 小东 66 使用手机短消息的方式,发送消息:[加急]:有人赞了你,给 小东 67 使用手机短消息的方式,发送消息:[特急]:有人赞了你,给 小东
---------------------------------------------------------------------
3、理解桥接模式
3.1、认识桥接模式
(1)什么是桥接
所谓桥接,通俗点时候就是在不同的东西之间搭一个桥,让它们能够连接起来,可以相互通讯和使用。那么在桥接模式中到底是给什么东西来搭桥呢?就是为被分离了的抽象部分和实现部分来搭桥,比如前面示例中的抽象的消息和具体消息发送之间搭个桥。
(ps: 在桥接模式中的桥接是单向的,也就是只能是抽象部分的对象去使用具体实现部分的对象,而不能反过来,也就是个单向桥。)
(2)为何需要桥接
为了达到让抽象部分和实现部分都可以独立变化的目的,在桥接模式中,是把抽象部分和实现部分分离开的,虽然从程序结构上是分开了,但是在抽象部分实现的时候,还是需要使用具体的实现的。所以需要搭个桥,让抽象部分通过这个桥就可以调用到实现部分的功能了,因此需要桥接。
(3)如何桥接
只要让抽象部分拥有实现部分的接口对象,就桥接上了,在抽象部分即可通过这个接口来调用具体实现部分的功能。也就是说,桥接在程序上体现了在抽象部分拥有实现部分的接口对象,维护桥接就是维护这个关系。
(4)独立变化
桥接模式的意图: 是使得抽象和实现可以独立变化,都可以分别扩充。也就是说抽象部分和实现部分是一种非常松散的关系。
(5)动态变换功能
由于桥接模式中的抽象部分和实现部分是完全分离的,因此可以在运行时动态组合具体的真实实现,从而达到动态变换功能的目的。
(6)桥接模式和继承
继承是扩展对象功能的一种常见手段,通常情况下,继承扩展的功能变化维护都是一维的,也就是变化的因素只有一类。
对于出现变化因素有两类的,也就是有两个变化纬度的情况,继承实现就会比较痛苦。比如上面的示例,就时有两个变化的纬度,一个是消息的类别,不同的消息类别处理不同;另外一个是消息的发送方式。
从理论上来说,如果用继承的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的乘积那么多个。比如上面的示例,在消息类别的纬度上,目前的可变数量是3个:普通消息、加急消息、特急消息;在消息发送方式的纬度上,目前的可变数量也是3个: 站内短消息、Email消息、手机短消息。在这种情况下,如果要实现全的话,那么需要的实现类应该是 3 * 3 = 9个。如果要在任何一个纬度上进行扩展,都需要实现另一个纬度上的可变数量那么多个实现类,这也是为何会感到扩展起来困难。
桥接模式就是用来解决这种有两个变化纬度的情况下, 如何灵活的扩展功能的一个很好的方案。其实,桥接模式主要是把继承改成了使用对象组合,从而把两个纬度分开,让每一个纬度单独去变化,最后通过对象组合的方式,把两个纬度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效的减少了实际实现的类的个数。
(7)桥接模式的调用顺序示意图
3.2、谁来桥接
所谓谁来桥接,就是谁来负责创建抽象部分和实现部分的关系,说得更直白点,就是谁来负责创建Implementor对象,并把它设置到抽象部分的对象中去。
大致有如下几种实现方式:
- 由客户端负责创建Implementor对象,并在创建抽象部分对象的时候,把它设置到抽象部分的对象中去。
- 可以在抽象部分对象构建的时候,由抽象部分的对象自己来创建相应的Implementor对象,可以给它传递一些参数,它可以根据参数来选择并创建具体的Implementor对象。
- 可以在Abstraction中选择并创建一个默认的Implementor对象,然后子类可以根据需要改变这个实现。
- 可以使用抽象工厂或者简单工厂来选择并创建具体的Implementor对象。
- 可以使用IoC/DI容器来创建具体的Implementor对象,并注入回到Abstraction中。
3.3、典型例子 -- JDBC
在Java应用中,对于桥接模式有一个非常典型的例子,就是: 应用程序使用JDBC驱动程序进行开发的方式。所谓驱动程序,指的是按照预先约定好的接口来操作计算机系统或者是外围设备的程序。
下面是JDBC操作的简单示例代码:
1 String sql = "具体要操作的sql语句"; 2 3 //1:装载驱动 4 Class.forName("驱动的名称"); 5 6 //2:创建连接 7 Connection conn = DriverManager.getConnection("连接数据库服务的URL","用户名","密码"); 8 9 //3:创建statement或者是preparedStatement 10 PreparedStatement pstmt = conn.prepareStatement(sql); 11 12 //4:执行sql,如果是查询,再获取ResultSet 13 ResultSet rs = pstmt.executeQuery(sql); 14 15 //5:循环从ResultSet中把值取出来,封装到数据对象中去 16 while(rs.next()){ 17 //取值示意,按名称取值 18 String uuid = rs.getString("uuid"); 19 //取值示意,按索引取值 20 int age = rs.getInt(2); 21 } 22 23 //6:关闭 24 rs.close(); 25 pstmt.close(); 26 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的这种架构,把抽象和具体分离开来,从而使得抽象和具体部分都可以独立扩展。
3.4、桥接模式的优缺点
优点:
- 分离抽象和实现部分,桥接分离了抽象部分和实现部分,从而极大地提高了系统的灵活性。让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统。
- 更好的扩展性,由于桥接模式把抽象部分和实现部分分离开了,而且分别定义接口,这就使得抽象部分和实现部分可以分别独立地扩展,而不会相互影响。
- 可动态地切换实现,由于桥接模式把抽象部分和实现部分分离了,所以在实现桥接的时候,就可以实现动态的选择和具体的实现。
- 可减少子类的个数。
缺点:
3.5、思考桥接模式
(1)桥接模式的本质:分离抽象和实现。
桥接模式最重要的工作就是分离抽象部分和实现部分,这是解决问题的关键。只有把抽象部分和实现部分分离开了,才能够让它们独立地变化;只有抽象部分和实现部分可以独立地变化,系统才能有更好的扩展性和可维护性。
(2)对设计原则的体现
- 桥接模式很好地实现了开闭原则
- 桥接模式很好地体现了: 多用对象组合,少用对象继承
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------