《Java Design Patterns》第八章 桥接模式 - 处理多维度变化

第八章 桥接模式 - 处理多维度变化

一、基本概念

1.1 场景

有两种文具,分别是毛笔和蜡笔。如果需要大中小三种型号的蜡笔,且每种型号需要有12种不同的颜色。

则蜡笔需要各种型号的各种颜色 3*12 = 36支,而毛笔只需要大中小型号和12中颜色的颜料盒 3 + 12 = 15支。

在蜡笔的模式中,无论是增加颜色,还是增加型号,都会影响另一个维度。耦合度非常的强。

但在毛笔中,型号与颜色进行了解耦,只是在使用时组合使用,非常灵活,扩展也十分方便。

1.2 场景二 - 跨平台图像浏览系统

现有一个系统,需要该系统支持BMP、JPG、GIF等格式的图形文件,并且能在Windows、Linux、MacOS等多个系统上运行。首先将文件解析为像素矩阵,然后各系统使用各自的绘制函数,将矩阵显示在屏幕上。

如果使用上面的“蜡笔模式” - 多层继承模式,可能会存在 Image -> BMPImage、JPGImage、GIFIMAGE -> BMPWindowsImage/BMPLinuxImage/BMPMacOSImage、JPGWindowsImage/JPGLinuxImage/JPGMacOSImage、GIFWindowsImage/GIFLinuxImage/GIFMacOSImage。

存在的问题:

  1. 由于采用了多层继承,系统中的类个数非常多。具体类的个数 = 所支持的图像格式 * 所支持的操作系统
  2. 系统扩展麻烦,如BMPWindowsImage,即包含图像文件格式,又包含操作系统信息。无论是增加图像格式还是操作系统,都需要增加大量具体类。
  3. 违反了“单一职责原则”,因为具体类将图像文件解析和像素矩阵显示这两个完全不同的职责融合,任意职责发生改变,都需要修改所有。

解决方案:

引入桥接模式,将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联。

二、桥接模式

2.1 定义

桥接模式,又称为柄体模式或接口模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。是一种非常实用的结构性设计模式。

如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。

桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使系统更加灵活,更易于扩展,同时有效控制了系统中类的个数。

桥接模式是一个非常有用的模式,包含了很多面向对象设计原则的思想,如“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏替换原则”、“依赖倒转原则”等。有助于我们深入理解设计原则,形成正确的设计思想和培养良好的设计风格。

2.2 桥接模式中的几个角色

  1. Abstraction - 抽象类
    1. 用于定义抽象类的接口,他一般是抽象类而不是接口
    2. 定义了一个 Implementor实现类接口 类型的对象,并可以维护该对象,它与实现类接口之间具有关联关系
    3. 既可以包含抽象业务方法,也可以包含具体业务方法
  2. RefinedAbstraction - 扩充抽象类
    1. 一般为具体类,实现了 Abstraction抽象类 中的抽象方法
    2. 因为 Abstraction抽象类 中定义了实现类接口对象,所以RefinedAbstraction扩充抽象类中可以调用 Implementor具体实现了类 中定义的业务方法
  3. Implementor - 实现类接口
    1. 定义实现类接口,提供基本操作
    2. 具体实现交给其子类
    3. 通过关联关系,在 Abstraction抽象类 中,可以直接调用 Implementor及其子类 的方法
    4. 使用关联关系来代替继承关系
  4. ConcreteImplementor - 具体实现类
    1. 实现 Implementor接口,不同的具体实现类提供不同的方法实现
    2. 用于提供给 Abstraction抽象类 调用的具体实现

2.3 具体实现

// 桥接模式
public class BridgePattern {

    // 像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
    static class Matrix {

    }

    // 抽象图像类:抽象类
    static abstract class Image {
        protected ImageImp imp;

        public void setImp(ImageImp imp) {
            this.imp = imp;
        }

        public abstract void parseFile(String fileName);
    }

    // JPG格式图像:扩充抽象类
    static class JPGImage extends Image {
        @Override
        public void parseFile(String fileName) {
            final Matrix matrix = new Matrix();
            imp.doPaint(matrix);
            System.out.println(fileName + ",格式为JPG");
        }
    }

    // PNG格式图像:扩充抽象类
    static class PNGImage extends Image {
        @Override
        public void parseFile(String fileName) {
            final Matrix matrix = new Matrix();
            imp.doPaint(matrix);
            System.out.println(fileName + ",格式为PNG");
        }
    }

    // 抽象操作系统实现类:实现类接口
    static interface ImageImp {
        public void doPaint(Matrix m); // 显示像素矩阵 m
    }

    // Windows 操作系统实现类:具体实现类
    static class WindowsImp implements ImageImp {
        @Override
        public void doPaint(Matrix m) {
            // 调用Windows系统的绘制函数,绘制像素矩阵
            System.out.println("在Windows操作系统中显示图像");
        }
    }

    // Linux 操作系统实现类:具体实现类
    static class LinuxImp implements ImageImp {
        @Override
        public void doPaint(Matrix m) {
            // 调用Linux系统的绘制函数,绘制像素矩阵
            System.out.println("在Linux操作系统中显示图像");
        }
    }

    public static void main(String[] args) {
        Image img = new PNGImage();
        ImageImp imp = new WindowsImp();
        img.setImp(imp);
        img.parseFile("死了都要爱.jpg");
    }
}

三、总结

在使用桥接模式时,我们应该先识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法,和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。

如毛笔,由于型号是固有的维度,可以设计为抽象毛笔类,在该类中声明并部分实现毛笔的业务方法,将各种型号的毛笔作为子类。将颜色作为毛笔的另一个维度。

增加新的毛笔类型,只需扩展左侧的“抽象部分”;增加新的颜色,只需扩展右侧的“实现部分”。

优点

  1. 分离抽象解耦及其实现部分。解耦了抽象和实现之间的绑定关系,使抽象部分和实现部分都有自己的维度变化
  2. 很多情况下,桥接模式可以取代多层继承方案。多层继承违背了“单一职责原则”,复用性较差,且类的个数较多
  3. 桥接模式提供了系统的可扩展性,两个维度的扩展都不需要修改原有系统,符合“开闭原则”

缺点

  1. 桥接模式的使用,会增加系统的理解和设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计和编程
  2. 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

适用场景

  1. 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系
  2. “抽象部分”和“实现部分”可以以继承的方式独立扩展而不受影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合
  3. 一个类存在两个(或多个)独立变化的维度,且各维度都需要独立进行扩展
  4. 对于不希望使用继承或多层继承的系统,导致系统类的个数急剧增加的系统,桥接模式非常适用

四、扩展

开发一个数据转换工具,可以将数据库中的数据转换成多种文件格式,例如txt、xml、pdf等格式,同时该工具需要支持多种不同的数据库。试使用桥接模式对其进行设计。

// 桥接模式
public class BridgePattern {

    // 数据库抽象类
    static abstract class Database {
        protected FileFormat fileFormat;

        public void setFileFormat(FileFormat fileFormat) {
            this.fileFormat = fileFormat;
        }

        public abstract void transition();
    }

    // Oracle 数据库 - 抽象扩展类
    static class OracleDatabase extends Database {
        @Override
        public void transition() {
            System.out.println("Oracle数据库");
            fileFormat.file();
        }
    }

    // Microsoft 数据库 - 抽象扩展类
    static class MCDatabase extends Database {
        @Override
        public void transition() {
            System.out.println("Microsoft数据库");
            fileFormat.file();
        }
    }

    // 文件格式 - 实现类接口
    interface FileFormat {
        void file();
    }

    // txt 文件 - 具体实现类
    static class TxtFileFormat implements FileFormat {
        @Override
        public void file() {
            System.out.println("TXT 格式文本输出");
        }
    }

    // pdf 文件 - 具体实现类
    static class PDFFileFormat implements FileFormat {
        @Override
        public void file() {
            System.out.println("PDF 格式文本输出");
        }
    }

    public static void main(String[] args) {
        Database db = new OracleDatabase();
        FileFormat ff = new TxtFileFormat();
        db.setFileFormat(ff);
        db.transition();
    }
}
文章作者: koral
文章链接: http://luokaiii.github.io/2019/07/08/读书笔记/《JavaDesignPatterns》/10.桥接模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自