桥接模式
一、模式的定义与特点
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
桥接(Bridge)模式的定义如下:将抽象与现实分离,使他们可以独立变化。它是用组合关系代替击沉骨干西来实现,从而降低了抽象和现实这两个可变维度的耦合度。
显然,桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。
优点
- 抽象与实现分离,扩展能力强
- 符合开闭原则
- 符合合成复用原则
- 其实现细节对客户透明
缺点
- 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确的识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
二、桥接模式的结构与实现
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
1.模式的结构
桥接模式包含以下主要角色:
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的自雷,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
其结构图如下
2.模式的实现
/**
* @author liyuan
* @date 2021年06月16日 17:48
* 抽象化角色
*/
abstract class Abstraction {
protected Implementor implementor;
protected Abstraction(Implementor implementor) {
this.implementor = implementor;
}
public abstract void operation();
}
/**
* @Author liyuan
* @Description 实现化角色
**/
public interface Implementor {
void operationImpl();
}
/**
* @author liyuan
* @date 2021年06月16日 17:54
* 具体实现化角色A
*/
public class ConcreteImplementorA implements Implementor{
@Override
public void operationImpl() {
System.out.println("具体实现化(Concrete Implementor)角色被访问");
}
}
/**
* @author liyuan
* @date 2021年06月16日 17:50
* 扩展抽象化角色
*/
public class RefinedAbstraction extends Abstraction{
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
public void operation(){
System.out.println("扩展抽象化(Refined Abstraction)角色被访问");
implementor.operationImpl();
}
}
public class Test {
public static void main(String[] args) {
// 采用ConcreteImplementorA这个实现
Implementor implementor = new ConcreteImplementorA();
// 采用RefinedAbstraction这个具体的抽象
Abstraction abstraction = new RefinedAbstraction(implementor);
abstraction.operation();
}
}
执行的结果如下:
扩展抽象化(Refined Abstraction)角色被访问
具体实现化(Concrete Implementor)角色被访问
三、桥接模式的应用实例
【例】用桥接模式来模拟女士包包的选购。
分析:女士皮包有很多种,可以按用途分、按皮质分、按品牌分、按颜色分、按大小分等,存在多个维度的变化,所以采用桥接模式来实现女士皮包的选购比较合适。
这里我们选用用途和颜色来模拟这个分类。
颜色类(color)是一个维度,定义为实现化角色,它有两个实现化角色:黄色和红色,通过getColor()方法来选择颜色。
包类(bag)是另外一个维度,定义为抽象化角色,它有两个扩展抽象化角色,挎包和钱包。它包含了颜色类对象,通过getName()方法可以选择相关颜色的挎包和钱包。
代码如下:
/**
* @author liyuan
* @date 2021年06月16日 18:08
* 实现化角色
*/
public interface Color {
String getColor();
}
/**
* @author liyuan
* @date 2021年06月16日 18:09
* 具体实现化角色红色
*/
public class ColorRed implements Color{
@Override
public String getColor() {
return "红色";
}
}
/**
* @author liyuan
* @date 2021年06月16日 18:09
* 具体实现化角色黄色
*/
public class ColorYellow implements Color{
@Override
public String getColor() {
return "黄色";
}
}
/**
* @author liyuan
* @date 2021年06月16日 18:11
* 抽象化角色
*/
abstract class Bag {
protected Color color;
public Bag(Color color) {
this.color = color;
}
public abstract String getName();
}
/**
* @author liyuan
* @date 2021年06月16日 18:13
* 扩展抽象化角色:挎包
*/
public class BagHand extends Bag {
public BagHand(Color color) {
super(color);
}
@Override
public String getName() {
return color.getColor()+"BagHand";
}
}/**
* @author liyuan
* @date 2021年06月16日 18:15
*/
public class BagWallet extends Bag{
public BagWallet(Color color) {
super(color);
}
@Override
public String getName() {
return color.getColor()+"BagWallet";
}
}
/**
* @author liyuan
* @date 2021年06月16日 18:17
*/
public class Test {
public static void main(String[] args) {
Color color = new ColorRed();
Bag bag = new BagHand(color);
System.out.println(bag.getName());
}
}
执行结果如下:
红色BagHand
四、桥接模式的应用场景
当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。
桥接模式通常适用于以下场景。
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。
桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。
因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
很多时候,我们分不清该使用继承还是组合/聚合或其他方式等,其实可以从现实语义进行思考。因为软件最终还是提供给现实生活中的人使用的,是服务于人类社会的,软件是具备现实场景的。当我们从纯代码角度无法看清问题时,现实角度可能会提供更加开阔的思路。
五、桥接模式的扩展
在软件开发中,有时桥接(Bridge)模式可与适配器模式联合使用。当桥接(Bridge)模式的实现化角色的接口与现有类的接口不一致时,可以在二者中间定义一个适配器将二者连接起来
【例】
需求一、就是多个报表系统与多个数据源,多个报表系统与多个数据源系统组合成多种报表展示,这样就出现了二维层次结构,这样我们就可以使用桥接设计模式来设计结构。
需求二、当这个系统需要扩充一个数据源,新增一个Hbase源,那么当前当前接口可能跟Bbase数据源接口不兼容,这样我们就可以使用适配器模式,将原有系统的接口与需要集成的接口整合在一起,是他们可以一起工作。
代码如下:
/**
* 报表展示的类 (实现化角色)
*/
public interface BirtShow {
//报表展示的方法
void show(String s);
}
/**
* @author liyuan
* @date 2021年06月17日 10:22
* 报表展示方式1(具体实现化角色1)
*/
public class BirtShowImpl1 implements BirtShow{
@Override
public void show(String s) {
System.out.println("报表1展示的内容为---"+s);
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:22
* 报表展示方式2(具体实现化角色2)
*/
public class BirtShowImpl2 implements BirtShow{
@Override
public void show(String s) {
System.out.println("报表2展示的内容为---" + s);
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:24
* 桥接抽象类。(抽象化角色)
*/
public abstract class BirtShowAbstract {
private BirtShow birtShow;
public BirtShowAbstract(BirtShow birtShow) {
this.birtShow = birtShow;
}
public void showBirt(String s) {
birtShow.show(s);
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:26
*
* 以JDBC为数据源来展示报表(具体抽象化角色jdbc)
*/
public class BirtShowJdbcData extends BirtShowAbstract{
public BirtShowJdbcData(BirtShow birtShow) {
super(birtShow);
}
public void showBirt() {
super.showBirt(new CollectAdapter("jdbc").collect("jdbc"));
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:27
*
* 以文本文件为数据源来展示报表(具体抽象化角色text)
*/
public class BirtShowTextData extends BirtShowAbstract{
public BirtShowTextData(BirtShow birtShow) {
super(birtShow);
}
//报表展示方法,里边调用了桥接抽象类的showBrit方法,传入具体的收集方式参数,这个参数就是我们写的适配器,适配多个数据源
public void showBirt() {
super.showBirt(new CollectAdapter("text").collect("text"));
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:16
* 目标接口
* 系统原来的收集方法接口,如果需要兼容第三方接口就需要写一个适配器,
* 使原来的收集方式与第三方的收集方式都兼容本系统
*/
public interface CollectText {
String collect(String s);
}
/**
* @author liyuan
* @date 2021年06月17日 10:17
* 模拟文本文件方式收集到的数据
* 对象适配者
*/
public class CollectTextIml implements CollectText{
@Override
public String collect(String s) {
return "我是文本文件收集方式";
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:16
* 目标接口
*/
public interface CollectJDBC {
String jdbcCollect();
}
/**
* @author liyuan
* @date 2021年06月17日 10:17
* 对象适配者
* 模拟JDBC收集到的数据
*/
public class CollectJDBCImpl implements CollectJDBC{
@Override
public String jdbcCollect() {
return "我是jdbc收集方式";
}
}
/**
* @author liyuan
* @date 2021年06月17日 10:18
* 适配器类
*/
public class CollectAdapter implements CollectText{
// 目标接口
private CollectJDBC collectJDBC;
private CollectText collect;
//根据不同的适配者创建其适配者的实现
public CollectAdapter(String type) {
if (type.equalsIgnoreCase("jdbc")) {
collectJDBC = new CollectJDBCImpl();
} else {
collect = new CollectTextIml();
}
}
/**
* 根据不同数据收集方式没获取不同数据源的数据
*
* @param type 标识
* @return 收集到的数据
*/
@Override
public String collect(String type) {
//判断是否是jdbc的方式收集数据
if (type.equalsIgnoreCase("jdbc")) {
return collectJDBC.jdbcCollect();
} else {
//否则就是以文本文件方式收集数据
return collect.collect(type);
}
}
}
public class Test {
public static void main(String[] args) {
BirtShowTextData showText = new BirtShowTextData(new BirtShowImpl1());
showText.showBirt();
BirtShowJdbcData showJdbc = new BirtShowJdbcData(new BirtShowImpl1());
showJdbc.showBirt();
BirtShowTextData showText1 = new BirtShowTextData(new BirtShowImpl2());
showText1.showBirt();
BirtShowJdbcData showJdbc1 = new BirtShowJdbcData(new BirtShowImpl2());
showJdbc1.showBirt();
}
}