设计模式之桥接模式:如何实现抽象协议与不同实现的绑定?

在上一讲中,我们学习了第一种结构型模式——适配器模式,它是在不改变目标类代码的情况下,通过引入适配器类来给目标类扩展功能。适配器模式在维护开发中经常会使用到,比如,常用在一些无法直接修改原有功能的旧系统里,开发一些新的扩展功能。

今天,我们继续学习另外一种结构型模式——桥接模式。桥接模式的原理非常简单,但是使用起来会有一定的难度,所以相对于适配器模式来说,在理解桥接模式时,我们学习的重点要能跳出局部,多从整体结构上去思考

话不多说,让我们正式开始今天的学习吧!

模式原理分析

桥接模式的定义是:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

不过,这里的抽象常常容易被理解为抽象类,并将实现理解为继承后的“派生类”,但是这样理解存在局限性,因为 GoF 的本意是想表达“从对象与对象间的关系去看,做抽象实体与抽象行为的分离”,所以使用抽象实体和抽象行为来描述更为准确

我们来看看桥接模式的 UML 描述,如下图所示:

image (9).png

从该图中,我们可以看到桥接模式主要包含了以下四个关键角色。

  • 抽象实体:定义的一种抽象分类。比如,电脑中的 CPU、内存、摄像头、显示屏等。

  • 具体实体:继承抽象实体的子类实体。比如,Intel i7 CPU、三星内存、徕卡摄像头、京东方显示屏幕等。

  • 抽象行为:定义抽象实体中具备的多种行为。比如,CPU 逻辑运算、内存读写存储、摄像头拍照、屏幕显示图像等。

  • 具体行为:实现抽象行为的具体算法。比如,Intel 使用 X64 架构实现 CPU 逻辑运算,Mac M1 芯片使用 ARM 架构实现 CPU 逻辑运算,等等。

在我看来,桥接模式原理的核心是抽象与抽象之间的分离,这样分离的好处就在于,具体的实现类依赖抽象而不是依赖具体,满足 DIP 原则,很好地完成了对象结构间的解耦。换句话说,抽象的分离间接完成了具体类与具体类之间的解耦,它们之间使用抽象来进行组合或聚合,而不再使用继承

下面我们再来看看桥接模式对应 UML 图的代码实现,具体如下:

public abstract class AbstractEntity {

    //行为对象

    protected AbstractBehavior myBehavior;

    //实体与行为的关联

    public AbstractEntity(AbstractBehavior aBehavior) {

        myBehavior = aBehavior;

    }

    //子类需要实现的方法

    public abstract void request();

}

public class DetailEntityA extends AbstractEntity {

    public DetailEntityA(AbstractBehavior aBehavior) {

        super(aBehavior);

    }

    @Override

    public void request() {

        super.myBehavior.operation1();

    }

}

public class DetailEntityB extends AbstractEntity {

    public DetailEntityB(AbstractBehavior aBehavior) {

        super(aBehavior);

    }

    @Override

    public void request() {

        super.myBehavior.operation2();

    }

}

public abstract class AbstractBehavior {

    public abstract void operation1();

    public abstract void operation2();

}

public class DetailBehaviorA extends AbstractBehavior{

    @Override

    public void operation1() {

        System.out.println("op-1 from DetailBehaviorA");

    }

    @Override

    public void operation2() {

        System.out.println("op-2 from DetailBehaviorA");

    }

}

public class DetailBehaviorB extends AbstractBehavior {

    @Override

    public void operation1() {

        System.out.println("op-1 from DetailBehaviorB");

    }

    @Override

    public void operation2() {

        System.out.println("op-2 from DetailBehaviorB");

    }

}

从上面的代码实现你会很容易发现,桥接模式封装了如下变化:

  • 实体变化;

  • 行为变化;

  • 两种变化之间的关系;

  • 变化引起的变化。

桥接模式封装变化的本质上是对事物进行分类(实体),并对实体中的功能性(行为)再划分的一种解决方案。比如,电子产品可以被分为手机、电脑等,其中手机隐藏了手机一类相关的变化;同样,手机和电脑都具备使用 App 软件的功能,它们各自隐藏了如何使用 App 的具体方式。在面向对象软件开发中,我们通常是使用接口或抽象类来作为抽象实体和具体实体,使用具体对象实例和实现接口的对象作为抽象行为和具体行为。

所以说,桥接模式的本质是通过对一个对象进行实体与行为的分离,来将需要使用多层继承的场景转换为使用组合或聚合的方式,进而解耦对象间的强耦合关系,达到对象与对象之间的动态绑定的效果,提升代码结构的扩展性

使用场景分析

一般来讲,桥接模式的常用场景有如下几种。

  • 需要提供平台独立性的应用程序时。 比如,不同数据库的 JDBC 驱动程序、硬盘驱动程序等。

  • 需要在某种统一协议下增加更多组件时。 比如,在支付场景中,我们期望支持微信、支付宝、各大银行的支付组件等。这里的统一协议是收款、支付、扣款,而组件就是微信、支付宝等。

  • 基于消息驱动的场景。 虽然消息的行为比较统一,主要包括发送、接收、处理和回执,但其实具体客户端的实现通常却各不相同,比如,手机短信、邮件消息、QQ 消息、微信消息等。

  • 拆分复杂的类对象时。 当一个类中包含大量对象和方法时,既不方便阅读,也不方便修改。

  • 希望从多个独立维度上扩展时。 比如,系统功能性和非功能性角度,业务或技术角度等。

  • 需要在运行时切换不同实现方法时。 比如,通过门面模式调用外部 RPC 服务。

接下来,我们通过一个不同操作系统下的文件上传例子来快速理解桥接模式的使用场景。

我们首先创建一个抽象实体类 FileUploader,它包含了两个抽象行为:上传(upload)和检查(check)。

public interface FileUploader {

    Object upload(String path);

    boolean check(Object object);



}

然后,我们再建立一个具体实体类 FileUploaderImpl,其中包含了抽象行为类 FileUploadExcutor(文件上传执行器),实现了抽象行为 upload 和 check。

public class FileUploaderImpl implements FileUploader {

    private FileUploadExcutor excutor = null;

    public FileUploaderImpl(FileUploadExcutor excutor) {

        this.excutor = excutor;

    }

    @Override

    public Object upload(String path) {

        return excutor.uploadFile(path);

    }

    @Override

    public boolean check(Object object) {

        return excutor.checkFile(object);

    }

}



public interface FileUploadExcutor {

    Object uploadFile(String path);

    boolean checkFile(Object object);

}

接下来,在 Linux 平台上实现文件上传执行器 LinuxFileUpLoadExcutor,在 Windows 上实现文件上传执行器 WindowsFileUpLoadExcutor,具体代码如下所示:

public class LinuxFileUpLoadExcutor implements FileUploadExcutor {

    @Override

    public Object uploadFile(String path) {

        return null;

    }

    @Override

    public boolean checkFile(Object object) {

        return false;

    }

}

public class WindowsFileUpLoadExcutor implements FileUploadExcutor {

    @Override

    public Object uploadFile(String path) {

        return null;

    }

    @Override

    public boolean checkFile(Object object) {

        return false;

    }

}

从上面的代码中,你会发现:通过将文件上传执行器和文件上传行为进行分离,就能实现实体和行为的灵活演化。比如,当你想要实现一个新的上传到云存储的文件上传执行器时,你可以先新建一个叫 OSSFileUploaderImpl 的具体实现类,然后建立对应的云存储文件执行器,接着再分别实现华为云、阿里云、腾讯云等各种不同云存储的文件上传执行器。如果你还想要在执行器里加入新的行为,比如删除,这时平台上的执行器并不需要调用“删除”这个接口,这样就做到了实体和行为的解耦,极大地提升了代码的扩展性。

细心的你可能已经发现了,当我们做了实体和行为的分离后,我们还可以结合更多的模式来扩展桥接模式。比如,这里我简单扩展了一下桥接模式的 UML 图:

image (10).png

在实现抽象行为时,我们可以使用上一讲介绍的适配器模式来扩展功能,也可以使用后面会学到的门面模式来扩展更多外部的服务功能。

总体来说,桥接模式的使用场景非常灵活,侧重于实体和行为的分离,然后再基于这两个维度进行独立的演化

Drawing 2.png

为什么要使用桥接模式?

分析完桥接模式的原理和使用场景后,我们再来说说使用桥接模式的原因,主要有以下三个。

第一个,为了灵活扩展代码结构。 上面使用了适配器模式和门面模式的桥接模式就是一个很好的思考方向,与通过硬编码直接调用 API 的形式相比,“通过模式来扩展”会更容易控制代码行数和逻辑结构。而从我多年的工作经验来看,在很多大规模的代码系统中,有结构的代码的可维护性会更好。因为是人来维护代码的,而人的特性是天生对结构型的东西更“敏感”,并且灵活的结构在后期进行代码重构时也能更好地替换与修改。

第二个,为了更好地解决跨平台兼容性问题。 桥接模式之所以能很好地解决跨平台的兼容性问题,就是因为桥接模式通过抽象层次上结构的分离,让相关的分类能够聚合到各自相关的层次逻辑中,而不同的平台对于同一个 API 在具体的代码实现上是不同的,这样反而符合不同操作系统按照各自维度演化的特性。

第三个,为了在运行时组合不同的组件。 无论是框架还是外部服务,我们都需要基于一个统一的协议进行协同工作,但是通过静态的继承方法很难做到在程序运行时进行方法或组件的动态更换。而使用桥接模式和门面模式就可以很方便地进行替换,比如,在上面文件上传执行器的案例中,我们可以使用一个统一的 API 网关调用不同的云服务来完成文件上传。

收益什么?损失什么?

使用桥接模式主要有以下四个大的优点。

  • 分离实体与行为,可以提升各自维度的演化效率。 比如,订单中的会员信息可以理解为抽象实体,普通会员和 plus 会员就是不同的具体实体;会员中的积分累计就是抽象行为,不同会员按照各自的积分计算轨迹进行计算就是具体行为的体现。那么,会员可以再继续增加更多会员类别,而积分计算规则也可以不断更新。

  • 符合开闭原则,提升代码复用性。 每一个维度的类都以组合或聚合关系进行合作,新增类或修改类都在各自类内部进行,不影响其他类。

  • 用组合关系替代了多重继承,提升了代码结构的演化灵活性。我们都知道多继承违背了单一职责原则,虽然关联性更强,但复用性很差。组合关系的优势就在于可以在任意阶段进行升级与替换,并且可以按需进行组合与撤销,这对于需求快速变化的开发场景而言很适用,能够极大地提升代码结构的灵活性。

  • 符合表达原则,提升代码的可理解性。由于桥接模式从抽象层次就进行了分离,不同的类别会按照各自的特点进行演化,所以不管是在结构上还是代码内在含义上,都更聚焦,这样在阅读代码时也就能更容易理解。

同样,桥接模式也有一些缺点。

  • 增加了维护成本。 桥接模式因为需要做很多实体和行为的分离,所以会间接地要增加不少代码行数。再加上使用组合和聚合关系不像继承关系那样容易找到对象之间的调用关系,稍不注意就会影响到其他对象,这样大大增加了代码修改维护的成本。

  • 导致性能下降。 组合或聚合关系在面向对象编程中使用的是委托的实现方式,简单理解就是调用的对象变多了,自然也就影响到了程序的性能。

  • 增加设计难度。 桥接模式更重视聚合而非继承关系,那么就需要建立更多的抽象层,要求开发者针对抽象层进行设计与编程。我们都知道,找到正确的抽象层有时是一件相当困难的事情,虽然现在有很多优秀的设计能够作为借鉴,但在一些新的领域里依然会有一定的设计难度。

总结

桥接模式可以说是 DIP 原则的具体实践。在软件开发中,一个对象可以从实体行为两个角度来进行分离,其实就是将依赖从一个大而全的对象变换到依赖两个可以独立变化的维度,控制也就发生了反转。

桥接模式因为重视组合和聚合,从而有效避免了多重继承带来的问题。也就是说,通过抽象实体与抽象行为的关联,将静态的继承关系转换为了动态的组合关系,从而使得系统结构更加灵活。

在实际开发中,你应该将桥接模式和更多的模式结合起来使用,将不同模式或服务作为某一个独立的维度来进行演化。另外,多在实践中寻找可以做实体和行为分离的场景,并尝试使用桥接模式来解决,这才是学习桥接模式最好的办法。

课后思考

在使用桥接模式新增加实体时,你认为能够复用现有抽象行为的可能性有多大?如果不能,那又会带来哪些问题呢?

欢迎留言分享,我会第一时间给你回复。

在下一讲,我会接着与你分享“组合模式:如何用树形结构处理对象之间的复杂关系?”这个话题,记得按时来听课!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值