桥接模式
1 实际场景
1.1 发送提示信息
考虑一个发送提示消息的实际业务功能。基本所有带业务流程处理的系统都会有这样的功能。
从业务上来看,消息可以分为普通消息、加急消息、特急消息多种。不同的消息类型,业务功能处理逻辑不一样。如:加急消息会在消息上添加加急;特急消息处理在消息上添加特急标识之外,还在会另外发送一条催促的消息。其中发送消息的方式可以分为系统消息,手机消息,邮件等。
如何实现?
1.2 非设计模式的解决方案
1.2.1 简化版本
先实现一个简化的功能:消息的类型只有普通类型,发送的方式只有站内消息和邮件。将问题简单化,方便后续说明。
由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如图所示:
消息的统一接口代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @author mark.hct
* @version Message.java v 0.1 2023/03/27 23:57 Exp $
* @description 消息的统一接口
*/
public interface Message {
/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
public void send(String message, String toUser);
}两种消息的实现类型代码如下:(这里只是做示意,简单实现消息的类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* @author mark.hct
* @version CommonMessageSMS.java v 0.1 2023/03/27 23:57 Exp $
* @description 以站内短消息的方式发送普通消息
*/
public class CommonMessageSMS implements Message {
public void send(String message, String toUser) {
System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser);
}
}
/**
* @author mark.hct
* @version CommonMessageEmail.java v 0.1 2023/03/27 23:58 Exp $
* @description 以Email的方式发送普通消息
*/
public class CommonMessageEmail implements Message {
public void send(String message, String toUser) {
System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser);
}
}
1.2.2 增加消息类型
上述简单版本的示例看着非常简单,若是还需要新增消息类型:加急信息,发送方式也是站内消息和邮件。其中与普通消息不同的是,加急消息会在消息内容中添加加急的前缀,然后在发送消息;另外加急消息会提供监控的方法,用户可以通过这个方法来查询加急消息的处理进度。因此加急消息需要扩展一个新的接口,处理发送消息的基本方法之外,还需要提供监控的功能,系统结构图如下:
需要扩展的加急消息的接口(监控功能)
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @author mark.hct
* @version UrgencyMessage.java v 0.1 2023/03/28 00:02 Exp $
* @description 加急消息的抽象接口
*/
public interface UrgencyMessage extends Message {
/**
* 监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
Object watch(String messageId);
}对应的两种消息发送方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38/**
* @author mark.hct
* @version UrgencyMessageSMS.java v 0.1 2023/03/28 00:03 Exp $
* @description
*/
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;
}
}
/**
* @author mark.hct
* @version UrgencyMessageEmail.java v 0.1 2023/03/28 00:04 Exp $
* @description
*/
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;
}
}- 在实现加急消息的实际过程中,一般是让加急消息处理的对象继承普通消息的实现,来满足发送不一样消息内容的需求,这里为了示例结构简单,没有这样做。
1.2.2 有何问题
上述做法中,也能满足需求。假设需要持续添加消息类型:特级消息,特级消息是在不同消息的处理基础上,添加催促功能,并且投递消息的方式还是站内消息和邮件,系统结构图如下:
通过系统结构图可以看出,通过继承的方式来扩展,会非常不方便。每次添加新的消息类型时,都需要将所有的消息投递方式都实现一遍(站内消息和邮件)。
若是新增投递方式,则需要对当前所有的消息类型都适配,如下系统结构图,新增了手机的消息投递方式,则需要对每个消息类型的具体实现中添加新的处理逻辑。
通过上述的例子可以直观地看出,扩展消息类型相当麻烦,不同的消息具有不同的实现。对于每一类的消息都有不同的发送消息的方式。而且如果要新增消息发送方式的时候,那么就要求所有的消息类型,都实现适配新的发送方式。
2 桥接模式
2.1 定义
适配器模式(Bridge Pattern
)属于结构型模式,将抽象部分与它的实现部分分离,使它们都可以独立地变化。
2.2 桥接模式解决问题思路
分析示例,可以消息拆分为两个维度:消息类型和发送消息的方式,其中消息类型有,普通消息、加急信息、特殊加急信息;发送消息方式有,站内信息、手机短信、Email。关系图如下:
前面分析扩展困难的原因是,消息的抽象和实现是混合在一起的,这就导致其中一个维度变化的话,则会引起另外一个维度的变化。
解决的方法就是将两个维度拆分开来,相互独立,这样可以实现独立的变化,是扩展变得简单。
桥接模式通过引入实现的接口,将实现部分从系统中分离出去,将顶层的抽象接口改成抽象类,在类里面持有一个具体的实现部分的实例,抽象部分与实现部分结合起来。
这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。
2.3 结构说明
Abstraction
:抽象部分的接口,通常在这个对象里面维护一个实现部分的对象引用,在抽象对象的方法中调用实现部分的完成,在这个方法中通常都是跟具体的业务相关的方法;RefinedAbstract
:扩展抽象部分的接口,通常在这些对象里面,定义与实际业务相关的方法,这些方法中会使用Abstraction
中定义的方法,也可能需要调用实现部分的对象来完成;Implementor
:定义实现部分的接口,这个接口不用和Abstraction
里面的方法一致,通常是由Implementor
接口提供基本的操作,而Abstraction
里面定义的是基于这些基本操作的业务方法,也就是说Abstraction
定义了基于这些基本操作的较高层次的操作;ConcreteImplementor
:实现Implementor
接口的对象;
2.4 示例代码
Implementor
接口定义1
2
3
4
5
6
7
8
9
10
11/**
* @author mark.hct
* @version Implementor.java v 0.1 2023/03/28 23:34 Exp $
* @description 定义实现部分的接口,可以与抽象部分接口的方法不一样
*/
public interface Implementor {
/**
* 示例方法,实现抽象部分需要的某些具体功能
*/
void operationImpl();
}Abstraction
抽象接口定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* @author mark.hct
* @version Abstraction.java v 0.1 2023/03/28 23:34 Exp $
* @description 定义抽象部分的接口
*/
public abstract class Abstraction {
/**
* 持有一个实现部分的对象
*/
protected Implementor impl;
/**
* 构造方法,传入实现部分的对象
*
* @param impl 实现部分的对象
*/
public Abstraction(Implementor impl) {
this.impl = impl;
}
/**
* 示例操作,实现一定的功能,可能需要转调实现部分的具体实现方法
*/
public void operation() {
impl.operationImpl();
}
}具体的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* @author mark.hct
* @version ConcreteImplementorA.java v 0.1 2023/03/28 23:37 Exp $
* @description 真正的具体实现对象
*/
public class ConcreteImplementorA implements Implementor {
public void operationImpl() {
//真正的实现
}
}
/**
* @author mark.hct
* @version ConcreteImplementorB.java v 0.1 2023/03/28 23:38 Exp $
* @description 真正的具体实现对象
*/
public class ConcreteImplementorB implements Implementor {
public void operationImpl() {
//真正的实现
}
}扩展
Abstraction
接口的对象实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* @author mark.hct
* @version RefinedAbstraction.java v 0.1 2023/03/28 23:39 Exp $
* @description 扩充由Abstraction定义的接口功能
*/
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor impl) {
super(impl);
}
/**
* 示例操作,实现一定的功能
*/
public void otherOperation() {
//实现一定的功能,可能会使用具体实现部分的实现方法,
//但是本方法更大的可能是使用Abstraction中定义的方法,
//通过组合使用Abstraction中定义的方法来完成更多的功能
}
}
2.5 重写方案
首先将消息的抽象和实现分离出来,分析要实现的功能,抽象部分就是消息类型对应的功能,而实现部分是发送消息的方式。
2.5.1 简化版本
桥接模式的结构图:
实现部分 - 定义的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @author mark.hct
* @version MessageImplementor.java v 0.1 2024/3/29 13:17 Exp $
* @description 实现发送消息的统一接口
*/
public interface MessageImplementor {
/**
* 发送消息
*
* @param message 要发送的消息内容
* @param toUser 消息发送的目的人员
*/
void send(String message, String toUser);
}抽象部分 - 定义的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* @author mark.hct
* @version AbstractMessage.java v 0.1 2024/3/29 13:17 Exp $
* @description 抽象的消息对象
*/
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);
}
}具体的实现,即消息的投递方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/**
* @author mark.hct
* @version CommonMessage.java v 0.1 2024/3/29 13:27 Exp $
* @description
*/
public class CommonMessage extends AbstractMessage {
public CommonMessage(MessageImplementor impl) {
super(impl);
}
public void sendMessage(String message, String toUser) {
// 对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了
super.sendMessage(message, toUser);
}
}
/**
* @author mark.hct
* @version UrgencyMessage.java v 0.1 2024/3/29 13:27 Exp $
* @description
*/
public class UrgencyMessage extends AbstractMessage {
public UrgencyMessage(MessageImplementor impl) {
super(impl);
}
public void sendMessage(String message, String toUser) {
super.sendMessage(message, toUser);
}
/**
* 扩展自己的新功能:监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}抽象部分的拓展,即消息的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
* @author mark.hct
* @version CommonMessage.java v 0.1 2024/3/29 13:27 Exp $
* @description
*/
public class CommonMessage extends AbstractMessage {
public CommonMessage(MessageImplementor impl) {
super(impl);
}
public void sendMessage(String message, String toUser) {
// 对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了
super.sendMessage(message, toUser);
}
}
/**
* @author mark.hct
* @version UrgencyMessage.java v 0.1 2024/3/29 13:27 Exp $
* @description
*/
public class UrgencyMessage extends AbstractMessage {
public UrgencyMessage(MessageImplementor impl) {
super(impl);
}
public void sendMessage(String message, String toUser) {
super.sendMessage(message, toUser);
}
/**
* 扩展自己的新功能:监控某消息的处理过程
*
* @param messageId 被监控的消息的编号
* @return 包含监控到的数据对象,这里示意一下,所以用了Object
*/
public Object watch(String messageId) {
//获取相应的数据,组织成监控的数据对象,然后返回
return null;
}
}
2.5.2 添加功能
在抽象部分(消息类型)添加一个特级消息,实现部分(消息投递方式)添加一个收集发送消息的方式。
特急消息的处理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* @author mark.hct
* @version SpecialUrgencyMessage.java v 0.1 2024/3/29 13:30 Exp $
* @description
*/
public class SpecialUrgencyMessage extends AbstractMessage {
public SpecialUrgencyMessage(MessageImplementor impl) {
super(impl);
}
public void sendMessage(String message, String toUser) {
//还需要增加一条待催促的信息
message = "特急:"+message;
super.sendMessage(message, toUser);
}
public void hurry(String messageId) {
//执行催促的业务,发出催促的信息
}
}手机消息投递方式的实现
1
2
3
4
5
6
7
8
9
10
11/**
* @author mark.hct
* @version MessageMobile.java v 0.1 2024/3/29 13:31 Exp $
* @description 以手机短消息的方式发送消息
*/
public class MessageMobile implements MessageImplementor {
public void send(String message, String toUser) {
System.out.println("使用手机短消息的方式,发送消息'"+message+"'给"+toUser);
}
}
2.5.3 功能测试
1 | /** |
执行结果:
1 | 使用站内短消息的方式,发送消息'请喝一杯茶'给小马 |
3 模式讲解
所谓桥接,就是把分离的抽象部分和实现部分进行连接(搭桥)
在桥接模式中的桥接是单向的,只能是抽象部分对象去使用具体实现部分的对象,不能反过来
只要让抽象部分拥有实现部分的接口对象,就属于桥接关系,再抽象部分中,就可以通过这个接口来调用具体的实现部分的功能,维护桥接,也就维护了这个关系。
桥接模式的意图:是的抽象和实现可以独立变化,可以分别扩充,两者之间是非常松散的关系,某种角度来讲,抽象和实现部分可以完全分开独立,抽象部分不过是一个使用实现部分对外接口的程序。类似于策略模式,抽象部分需要根据某个策略,来选择真实的实现,也就是说桥接模式的抽象部分相当于策略模式的上下文。更原始的就直接类似于面向接口编程,通过接口分离的两个部分而已。但是,桥接模式的抽象部分,是可以继续扩展和变化,而策略模式只有上下文,是不存在所谓抽象部分。
那抽象和实现为何还要组合在一起呢?原因是在抽象部分和实现部分还是存在内部联系,抽象部分的实现通常是需要调用实现部分的功能来实现。
3.1 退化的桥接模式
如果 Implementor
仅有一个实现,那么就没有必要创建 Implementor
接口了,这是一种桥接模式退化的情况。这个时候 Abstraction
和 Implementor
是一对一的关系,虽然如此,也还是要保持它们的分离状态,这样的话,它们才不会相互影响,才可以分别扩展。
3.2 桥接实现分析
如何拆分需求中的抽象部分和实现部分,如上述例子中,谁来负责创建 Implementor
的对象,谁来抽象部分,有以下几种方式:
1、由抽象部分的对象自己来创建相应的 Implementor 的对象
a. 从外面传递参数比较简单,如上述示例,如果用一个 type
来标识具体采用哪种发送消息的方案,然后在 Abstraction
的构造方法中,根据 type
进行创建;
1 | /** |
b. 对于不需要外部传入参数的情况,那就说明是在 Abstraction 的实现中,根据具体的参数数据来选择相应的 Implementor 对象。有可能在 Abstraction 的构造方法中选,也有可能在具体的方法中选择;
比如前面的示例,如果发送的消息长度在 100 以内采用手机短消息,长度在 100-1000 采用站内短消息,长度在 1000 以上采用 Email,那么就可以在内部方法中自己判断实现。
1 | public abstract class AbstractMessage { |
总结:优点是客户使用简单,而且集中控制 Implementor 对象的创建和切换逻辑;缺点是要求 Abstraction 知道所有的具体的 Implementor 实现,并知道如何选择和创建它们
2、在 Abstraction 中创建缺省的 Implementor 对象
对于这种方式,实现比较简单,直接在 Abstraction
的构造方法中,创建一个缺省的 Implementor
对象,然后子类根据需要,看是直接使用还是覆盖掉。示例代码如下:
1 | public abstract class AbstractMessage { |
3、使用抽象工厂或者是简单工厂
对于这种方式,根据具体的需要来选择,如果是想要创建一系列实现对象,那就使用抽象工厂,如果是创建单个的实现对象,那就使用简单工厂就可以了。
直接在原来创建 Implementor
对象的地方,直接调用相应的抽象工厂或者是简单工厂,来获取相应的 Implementor
对象,这种方法的优点是 Abstraction
类不用和任何一个 Implementor
类直接耦合。
4、使用 IoC/DI 的方式
对于这种方式,Abstraction
的实现就更简单了,只需要实现注入 Implementor
对象的方法就可以了,其它的 Abstraction
就不管了。
IoC/DI
容器会负责创建 Implementor
对象,并设置回到 Abstraction
对象中,使用 IoC/DI
的方式,并不会改变 Abstraction
和 Implementor
的关系,Abstraction
同样需要持有相应的 Implementor
对象,同样会把功能委托给 Implementor
对象去实现。
3.3 桥接模式的优缺点
- 分离抽象和实现部分:桥接模式分离了抽象和实现部分,从而极大地提高了系统的灵活性。让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统。对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了;
- 更好的扩展性:由于桥接模式把抽象和实现部分分离开了,而且分别定义接口,这就使得抽象部分和实现部分可以分别独立的扩展,而不会相互影响,从而大大的提高了系统的可扩展性。可动态切换实现;
- 可减少子类的个数:如文章一开头的例子,对于有两个变化纬度的情况,如果采用继承的实现方式,大约需要两个纬度上的可变化数量的乘积个子类;而采用桥接模式来实现的话,大约需要两个纬度上的可变化数量的和个子类。可以明显地减少子类的个数;
3.3 使用场景
- 不希望抽象和实现部分使用固定的绑定关系:把抽象和实现部分分开,然后在程序运行期间来动态的设置抽象部分需要用到的具体的实现,还可以动态切换具体的实现;
- 出现抽象部分和实现部分都应该可以扩展的情况:让抽象部分和实现部分可以独立的变化,从而可以灵活的进行单独扩展,而不是搅在一起,扩展一边会影响到另一边;
- 希望实现部分的修改,不会对客户产生影响:客户是面向抽象的接口在运行,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,说对客户是透明的;
- 采用继承的实现方案,会导致产生很多子类:可以考虑采用桥接模式,分析功能变化的原因,看看是否能分离成不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目;