从零开始学习Java设计模式 | 结构型模式篇:桥接模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 结构型模式篇:桥接模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下结构型模式里面的第四个设计模式,即桥接模式。

概述

在向大家讲解桥接模式之前,我们先来看一个例子。

现在有一个需求,需要创建不同的图形,例如圆、长方形、正方形等等,并且每个图形都有可能会有不同的颜色,这样,我们就可以利用继承的方式来设计类之间的关系了。

我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色的话,你会发现需要创建更多的子类,极有可能会出现类爆炸的现象。

试想,在一个有多种可能会变化的维度的系统中,用继承方式势必就会造成类爆炸的现象,扩展起来也不灵活,即使它满足了开闭原则。每次在一个维度上新增一个具体实现都要增加多个子类。此时,为了更加灵活的设计系统,我们便可以考虑使用桥接模式了。

那什么是桥接模式呢?下面我们来看一看它的概念。

桥接模式是指将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

有些同学看了桥接模式的概念之后,不明白什么叫抽象和实现,所以这里我给大家稍微解释一下。其实,抽象和实现就是两个维度,以上面的例子来说,图形的变化就是一个维度,而每一个图形颜色的变化就是另外一个维度,因此抽象和实现指的就是这两种维度的不同的变化。

结构

桥接模式的概念了解清楚之后,下面我们来看一下桥接模式里面有哪些角色?

桥接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)角色:定义抽象类,并且该抽象类还包含了一个对实现化对象的引用,也就是说该抽象类把实现化角色对象给聚合进来了
  • 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。因为在父类中已经聚合了实现化角色对象,所以我们就可以调用实现化角色对象里面的业务方法了
  • 实现化(Implementor)角色:定义实现化角色的接口(注意,这里可以是接口也可以是抽象类),供扩展抽象化角色调用
  • 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现

以上角色可能大家还不是特别理解,等会我们就通过一个具体的案例来深入理解一下桥接模式的概念以及其里面的角色。

桥接模式案例

接下来,我就以一个视频播放器的案例来为大家讲解桥接模式了。

分析

我们先来看一下具体的需求:现在需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,而常见的视频文件格式包括RMVB、AVI、WMV等,这样,该播放器就包含了两个维度,操作系统属于一个维度,因为操作系统有分Windows、Mac、Linux等,视频文件又属于一个维度,因为视频文件有RMVB、AVI、WMV等格式的。所以,在这里我们就能使用桥接模式进行一个实现了。

然后,大家来看一下下面的这张类图。

在以上类图的右边部分可以看到,有一个VideoFile接口,该接口就是实现化角色,当然了,你也可以将其定义成抽象类,只不过在这儿咱们是以接口的形式体现出来的。而且,该接口里面定义有一个decode方法专门用于进行解码,从上图中可以看到,解码时我们还需要向decode方法里面传递一个字符串类型的参数(即fileName),也就是视频文件的文件名。

从上图中还可以看到,VideoFile接口还有两个子实现类,一个是AVIFile,一个是RMVBFile,它俩都重写了父接口中的抽象方法,所以很明显它俩就是具体实现化角色。

明确以上类图的右边部分之后,咱们再来看一下左边部分。首先,有一个操作系统类(即OperatingSystem),它聚合了VideoFile接口,这也就意味着它是一个抽象化角色,因为抽象化角色是需要聚合实现化角色的。而桥接模式的巧妙之处主要就体现在这。

此外,该操作系统类同样也有两个子类,一个是Windows,一个是Mac,它俩都有各自对应的有参构造,并且它俩都重写了父类中的play方法,以便进行视频文件的一个播放。

以上类图分析完了以后,接下来,我们就要开始编写代码实现以上案例了。

实现

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即bridge,也即桥接模式的具体代码我们是放在了该包下。

然后,创建一个接口,该接口我们就命名为VideoFile。

package com.meimeixia.pattern.bridge;

/**
 * 视频文件(实现化角色)
 * @author liayun
 * @create 2021-07-31 16:38
 */
public interface VideoFile {

    // 解码功能
    void decode(String fileName);

}

接着,创建VideoFile接口的子实现类。我们先创建第一个子实现类,名字就起为AviFile,在该子实现类里面我们肯定是要去重写其父接口中的decode方法的。

package com.meimeixia.pattern.bridge;

/**
 * avi视频文件(具体的实现化角色)
 * @author liayun
 * @create 2021-07-31 16:42
 */
public class AviFile implements VideoFile {
    @Override
    public void decode(String fileName) {
        System.out.println("avi视频文件:" + fileName);
    }
}

再创建第二个子实现类,名字就起为RmvbFile,同理,该子实现类也要去重写其父接口中的decode方法。

package com.meimeixia.pattern.bridge;

/**
 * rmvb视频文件(具体的实现化角色)
 * @author liayun
 * @create 2021-07-31 16:45
 */
public class RmvbFile implements VideoFile {
    @Override
    public void decode(String fileName) {
        System.out.println("rmvb视频文件:" + fileName);
    }
}

截至到这里,我们就定义好了实现化角色和具体实现化角色。

紧接着,创建操作系统类,这里我们将其命名为OperatingSystem,注意,该类是一个抽象类。

在该类里面,我们要先声明VideoFile接口类型的变量,因为表示抽象化角色的该类要聚合表示实现化角色的VideoFile接口,这一点我在分析以上类图时就已经讲过了。注意,在OperatingSystem类里面声明VideoFile接口类型的变量时,我们不妨就使用protected权限修饰符,此时,只有其子类才可以直接继承使用。

声明完之后,在OperatingSystem类里面我们还得为其提供一个有参的构造方法,通过该有参构造给VideoFile接口类型的变量赋值。

不要忘了,在OperatingSystem类里面我们还得定义一个抽象的方法,专门用于播放视频文件。大家可能会问,为什么该方法要定义成抽象的呢?因为我们现在暂时还不明确该操作系统到底是Windows操作系统呢,还是Mac操作系统。

这样,OperatingSystem类的代码就呼之欲出了。

package com.meimeixia.pattern.bridge;

/**
 * 抽象的操作系统类(抽象化角色)
 * @author liayun
 * @create 2021-07-31 16:53
 */
public abstract class OperatingSystem {
    // 声明VideoFile变量
    protected VideoFile videoFile;

    public OperatingSystem(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);
}

抽象化角色定义好之后,接下来,我们来定义扩展抽象化角色。

从以上类图中得知,表示扩展抽象化角色的类有两个,一个是Windows,一个是Mac,它俩都得继承以上OperatingSystem类,并重写它里面的抽象方法(即play)。问题是如何重写该抽象方法呢?很简单,如果要播放视频文件的话,那么直接调用VideoFile接口类型对象里面的decode方法对视频文件进行一个解码就可以播放了,是不是啊!除此之外,它俩还得提供各自对应的有参构造为父类中声明的VideoFile接口类型的变量进行赋值。

这样,Windows操作系统类的代码就应该是下面这个样子的了。

package com.meimeixia.pattern.bridge;

/**
 * Windows操作系统(扩展抽象化角色)
 * @author liayun
 * @create 2021-07-31 16:58
 */
public class Windows extends OperatingSystem {

    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }

}

同理,Mac操作系统类的代码就不难写出来了。

package com.meimeixia.pattern.bridge;

/**
 * Mac操作系统(扩展抽象化角色)
 * @author liayun
 * @create 2021-07-31 17:02
 */
public class Mac extends OperatingSystem {

    public Mac(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }

}

最后,我们来创建一个客户端类进行测试。

package com.meimeixia.pattern.bridge;

/**
 * @author liayun
 * @create 2021-07-31 17:08
 */
public class Client {
    public static void main(String[] args) {
        // 创建Mac操作系统对象,注意,在创建时我们还得传递一个具体实现化角色对象,也就是告诉操作系统应播放什么格式的视频文件
        OperatingSystem system = new Mac(new AviFile());
        // 使用操作系统播放视频文件
        system.play("战狼3");
    }
}

此时,运行以上客户端类的测试代码,如下图所示,打印结果是avi视频文件:战狼3,这说明Mac操作系统可以播放AVI格式的视频文件。

当然,如果你要去播放其它格式(比如RMVB)的视频文件的话,那么在创建Mac操作系统对象时,给其传递RmvbFile对象就可以了。

至此,使用桥接模式我们就实现了以上案例,通过这个案例,相信大家对桥接模式的概念及其所包含的角色有了一个更加深刻的认识,因为桥接模式里面所包含的角色确实有点抽象,令人难以理解!

桥接模式的好处以及使用场景

接下来,我们来看看桥接模式的好处以及使用场景。

好处

桥接模式的好处,我总结出来了下面两个。

  1. 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

    例如,如果现在还有一种视频文件类型WMV,那么我们只需要再定义一个类实现VideoFile接口即可,其他类则不需要发生变化;如果现在还有一种操作系统Linux,那么我们只需要再定义一个类继承OperatingSystem抽象类即可,其他类则不需要发生变化。这样,系统的可扩展性就会比较好,而且我们在去添加子类时,也不需要添加太多的类,在这一过程中,由于其他类不需要发生变化,所以也满足了开闭原则

  2. 实现细节对客户透明

使用场景

桥接模式的使用场景,我总结出来了下面三个。

  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时

  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时

  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。此时,我们应避免在两个层次之间建立静态的继承联系,而是通过桥接模式使它们在抽象层建立一个关联关系,注意了,这里面主要指的是聚合关系。

    总之,应尽量避免去使用继承,因为上面已经说过了,虽然使用继承确实可行,但是它会导致类爆炸的现象发生

以上是关于从零开始学习Java设计模式 | 结构型模式篇:桥接模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:装饰者模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式

从零开始学习Java设计模式 | 结构型模式篇:外观模式