概念
如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
以日常生活中的毛笔和蜡笔为例,假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色。
那么使用蜡笔时,我们需要准备36支笔。
但在使用毛笔时,只需要三种型号的笔和12个颜料盒。
上面的蜡笔就是把多种功能聚合在一起,导致系统中存在了较强的耦合性。而毛笔则把颜色和型号解耦,使用起来更为灵活
UML类图
- Client : 桥接模式的调用者
- Abstraction(抽象类): 用于定义抽象类的接口。一般是抽象类而不是接口,持有了一个Implementor类的实例
- RefinedAbstraction(扩充抽象类): 扩充由Abstraction定义的接口,是Abstraction的子类,实现了Abstraction中声明的抽象业务方法,也可以调用Implementor中定义的业务方法
- Implementor(实现类接口): 定义实现类的接口。
- ConcreteImplementor(具体实现类): 在不同实现类中提供基本操作不同的实现
应用场景
- 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
具体应用
场景:以文件导出为例,需要提供一个文件转换器,支持从数据库中读取数据,导出成txt、excel等格式的文件,同时也支持各种不同的数据库(假设只有mysql 和 sqlServer)。
按照我们的结构图,我们需要先定义好抽象类和实现类。一般我们把2个维度中与业务方法关系最为密切的维度设计为抽象类,而另一个设计为实现类。
因为我们设计的文件转换器,所以这里的抽象类时文件转换器,而实现类是数据加载器
首先定义一个数据加载器接口:
public interface DataLoaderImplementor {
void loadData();
}
其次定义抽象的文件转换器:
public abstract class AbstractDataConvertor {
protected DataLoaderImplementor dataLoader;
public AbstractDataConvertor(DataLoaderImplementor dataLoader){
this.dataLoader = dataLoader;
}
protected void convert(){}
}
接下来定义扩充的抽象类,继承自抽象的文件转换器,因为我们要导出txt或者excel的文件,所以我们定义2个扩充抽象类
TxtDataConvertor :用于转换导出为txt格式文件
public class TxtDataConvertor extends AbstractDataConvertor {
public TxtDataConvertor(DataLoaderImplementor dataLoader) {
super(dataLoader);
}
@Override
protected void convert() {
dataLoader.loadData();
System.out.println("转换为txt格式文件");
}
}
ExcelDataConvertor:用于转换导出为excel格式文件
public class ExcelDataConvertor extends AbstractDataConvertor {
public ExcelDataConvertor(DataLoaderImplementor dataLoader) {
super(dataLoader);
}
@Override
protected void convert() {
dataLoader.loadData();
System.out.println("转换为excel格式文件");
}
}
接下来我们定义数据加载器的实现,因为要支持mysql和sqlserver, 所以同样我们也会定义2个具体实现类
MySqlDataLoaderImpl:用于从mysql数据库读取数据
public class MySqlDataLoaderImpl implements DataLoaderImplementor {
@Override
public void loadData() {
System.out.println("从mysql数据库读取数据");
}
}
SqlServerDataLoaderImpl:用于从sql server 数据库读取数据
public class SqlServerDataLoaderImpl implements DataLoaderImplementor {
@Override
public void loadData() {
System.out.println("从sql server 数据库读取数据");
}
}
最后定义客户端调用类
public class Client {
public static void main(String[] args) {
//从mysql数据库中读取数据转换为txt文件
TxtDataConvertor txtDataConvertor = new TxtDataConvertor(new MySqlDataLoaderImpl());
txtDataConvertor.convert();
//从sqlserver数据库中读取数据转为excel文件
ExcelDataConvertor excelDataConvertor = new ExcelDataConvertor(new SqlServerDataLoaderImpl());
excelDataConvertor.convert();
}
}
查看运行结果:
桥接模式优缺点
优点
- 分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
- 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
- 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
缺点
- 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。