一、桥接模式(Bridge Pattern)
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
1. 什么是桥接模式
桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接(Bridge)模式的优点是:
- 由于抽象与实现分离,所以扩展能力强;
- 其实现细节对客户透明。
缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,这增加了系统的理解与设计难度。
2. 桥接模式的优缺点
优点:
- 分离抽象部分及其实现部分,提供了比集成更好解决方案;
- 提高了系统的可扩展性,某种程度上可以避免子类爆炸
- 符合开闭原则
- 符合合成复用原则,通过组合方式实现关联
缺点:
- 增加了系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
- 需要正确地识别出系统中两个独立变化的纬度,因此其使用范围具有一定的局限性
3. 桥接模式的适用场景
- 如果一个系统需要再构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使他们在抽象层建立一个关联关系
- 对于那些不希望使用继承或因为多层次继承导致类的个数急剧增加的系统,桥接模式尤为适用;
- 一个类存在两个(或多个)独立变化的纬度,且这两个(或多个)纬度都需要独立进行扩展
- 不希望使用继承,或因为多层继承导致系统类的个数剧增
4. 相关设计模式
桥接模式与组合模式的区别
- 组合模式:强调部分与整体的组合
- 桥接模式:强调平行类之间的组合
桥接模式与适配器模式的区别
- 适配器:改变已有接口,使他们相互配合,把功能相似但接口不同的类适配起来
- 桥接:类的抽象和实现分离开,目的就是分离,让多个类能够自由组合
- 二者的共同点是这两个模式都是为了让两个类配合工作,但二者目的不同
二、桥接模式的结构与实现
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
1. 桥接模式的结构
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
其结构图如图 1 所示。
2. 桥接模式的实现
桥接模式的实现步骤
- 定义实现化角色接口 Implementor
- 定义具体实现化角色类 ConcreteImplementorA、ConcreteImplementorB
- 定义抽象化角色抽象类 Abstraction,以组合方式关联实现化角色接口
- 定义扩展抽象化角色类 RefinedAbstraction
- 编写客户端类 BridgePatternTest,调用具体实现化角色类、扩展抽象化角色类
实现代码示例:
-
定义实现化角色接口 Implementor
/** * Implementor - 实现化角色 * 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用 * * @author dongrui * @date 2020/2/25 11:03 */ public interface Implementor { void operationImpl(); }
-
定义具体实现化角色类 ConcreteImplementorA、ConcreteImplementorB
/** * ConcreteImplementorA - 具体实现化角色 A * 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现 * * @author dongrui * @date 2020/2/25 11:05 */ public class ConcreteImplementorA implements Implementor{ @Override public void operationImpl() { System.out.println("使用 A 方式实现"); } }
/** * ConcreteImplementorB * 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现 * * @author dongrui * @date 2020/2/25 11:08 */ public class ConcreteImplementorB implements Implementor { @Override public void operationImpl() { System.out.println("使用 B 方式实现"); } }
-
定义抽象化角色抽象类 Abstraction,以组合方式关联实现化角色接口
/** * Abstraction - 抽象化角色 * 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用 * * @author dongrui * @date 2020/2/25 11:09 */ public abstract class Abstraction { //使用组合方式关联实现化角色 protected Implementor implementor; protected Abstraction(Implementor implementor) { this.implementor = implementor; } /** * 供客户端调用的业务方法 */ abstract void operation(); }
-
定义扩展抽象化角色类 RefinedAbstraction
/** * RefinedAbstraction - 扩展抽象化角色,实现父类中的业务方法 * 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法, * 并通过组合关系调用实现化角色中的业务方法。 * * @author dongrui * @date 2020/2/25 11:12 */ public class RefinedAbstraction extends Abstraction { public RefinedAbstraction(Implementor implementor) { super(implementor); } /** * 供客户端调用的业务方法 */ @Override void operation() { System.out.println(this.implementor.getClass()); this.implementor.operationImpl(); } }
-
编写客户端类 BridgePatternTest,调用具体实现化角色类、扩展抽象化角色类
/** * BridgePatternTest - 客户端类 * * @author dongrui * @date 2020/2/25 11:17 */ public class BridgePatternTest { public static void main(String[] args) { Implementor implementorA = new ConcreteImplementorA(); Implementor implementorB = new ConcreteImplementorB(); Abstraction abs = new RefinedAbstraction(implementorA); abs.operation(); } }
三、桥接模式的案例实战
1. 案例1—银行存款
银行存款业务场景:小张有一笔钱想用于理财投资,为此做了一番功课了解到,现在有中国工商银行、中国农业银行两家银行,每家银行都提供了活期存储和定期存款,活期存款存取灵活但利息低,定期存款利息相对较高但存取不够灵活,同时了解到中国工商银行的定期存款利率相对较高,中国农业银行的活期存款利率相对较高,于是小张计划将投资款项分为2部分,一部分存储到中国工商银行的定期账户,一部分存储到中国农业银行的活期账户。
请根据以上业务设计一个银行存款系统,提供灵活的配置功能。
1.1 业务场景分析
从业务场景可以知道,有多家银行,每家银行都有自己的定期和活期账户,而客户可以对银行、存款类型做随机组合,因此,这种多纬度组合的场景非常适合使用桥接模式解决。
桥接模式的核心是将抽象与实现分离,使他们可以独立变化,在该模式中,活期存款、定期存款是银行存款的最终实现部分,是桥接模式的实现部分,中国工商银行、中国农业银行等银行是抽象部分,不提供实现操作,通过组合存款账户方式实现存款的实现操作。
因此,结合桥接模式设计银行存款:
- 定义实现:存款账户是实现部分,抽象出存款账户类为实现化接口,活期存储、定期存款为其具体实现
- 定义抽象:银行是抽象部分,抽象出银行类为抽象类,以成员属性方式组合存储账户,中国工商银行、中国农业银行为其扩展实现
- 定义客户端:在客户端类中选择具体账户和具体银行,实现存款业务逻辑
1.2 UML类图
1.3 代码实现示例
代码实现步骤:
- 定义存款账户接口
- 定义活期存款账户实现类、活期存款账户实现类,都实现存款账户接口并重写接口的方法
- 定义银行抽象类,将存款账户作为成员属性,实现对存款操作的组合方式调用
- 定义中国工商银行、中国农业银行的扩展类,实现具体的存款操作
- 定义客户端测试类,选择中国工商银行存储定期存款,选择中国农业银行存储活期存款
代码实现示例:
-
定义存款账户接口
/** * Account - 账户接口 * * @author dongrui * @date 2020/2/20 16:20 */ public interface Account { void saveMoney(Double money); void printAccountType(); }
-
定义活期存款账户实现类、活期存款账户实现类,都实现存款账户接口并重写接口的方法
/** * DepositAccount - 定期存款账户 * * @author dongrui * @date 2020/2/20 16:23 */ public class DepositAccount implements Account { public void saveMoney(Double money) { System.out.println("打开定期账号"); System.out.printf("存款:%f 元\n", money); } public void printAccountType() { System.out.println("这是一个定期账号"); } }
/** * SavingsAccount - 活期存储账户 * * @author dongrui * @date 2020/2/20 16:24 */ public class SavingsAccount implements Account { public void saveMoney(Double money) { System.out.println("打开活期账号"); System.out.printf("存款:%f 元\n", money); } public void printAccountType() { System.out.println("这是一个活期账号"); } }
-
定义银行抽象类,将存款账户作为成员属性,实现对存款操作的组合方式调用
/** * Bank - 银行抽象类,以组合方式调用存款账户接口 * * @author dongrui * @date 2020/2/20 16:25 */ public abstract class Bank { protected Account account; public Bank(Account account) { this.account = account; } abstract void saveMoney(Double money); }
-
定义中国工商银行、中国农业银行的扩展类,实现具体的存款操作
/** * ICBCBank * * @author dongrui * @date 2020/2/20 16:27 */ public class ICBCBank extends Bank { public ICBCBank(Account account) { super(account); } void saveMoney(Double money) { System.out.println("打开中国工商银行账号"); this.account.saveMoney(money); } }
/** * ABCBank * * @author dongrui * @date 2020/2/20 16:26 */ public class ABCBank extends Bank { public ABCBank(Account account) { super(account); } void saveMoney(Double money) { System.out.println("打开中国农业银行账号"); account.saveMoney(money); } }
-
定义客户端测试类,选择中国工商银行存储定期存款,选择中国农业银行存储活期存款
public class BankSavingsTest { public static void main(String[] args) { //定期账号和活期账号 Account depositAccount = new DepositAccount(); Account savingsAccount = new SavingsAccount(); //工商银行定期账号 Bank icbc = new ICBCBank(depositAccount); icbc.saveMoney(20000.00); System.out.println("------"); //农业银行活期账号 Bank abc = new ABCBank(savingsAccount); abc.saveMoney(3000.00); } }
附录
参考资料:
- 桥接模式(Bridge模式)详解 : http://c.biancheng.net/view/1364.html
- 桥接模式 : https://www.runoob.com/w3cnote/bridge-pattern2.html
- 设计模式读书笔记-----桥接模式: https://www.cnblogs.com/chenssy/p/3317866.html