一口一口啃完Java中的24种设计模式---桥接模式


问题引入:蜡笔与毛笔

在正式介绍桥接模式之前,先谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小 3 种型号的画笔,能够绘制 12 种不同的颜色,如果使用蜡笔,需要准备 3×12 = 36 支,但如果使用毛笔的话,只需要提供 3 种型号的毛笔,外加 12 个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与 36 支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有 12 种颜色,对应的蜡笔需增加 12 支,而毛笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况.

跨平台图像浏览系统

开发一个跨平台图像浏览系统,要求该系统能够显示 BMP、JPG、GIF、PNG 等多种格式的文件,并且能够在 Windows、Linux、Unix 等多个操作系统上运行。系统首先将各种格式的文件解析为像素矩阵(Matrix),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。系统需具有较好的扩展性以支持新的文件格式和操作系统。

某公司的开发人员针对上述要求,提出了一个初始设计方案,其基本结构如图所示:

在图的初始设计方案中,使用了一种多层继承结构,Image 是抽象父类,而每一种类型的图像类,如 BMPImage、JPGImage 等作为其直接子类,不同的图像文件格式具有不同的解析方法,可以得到不同的像素矩阵;由于每一种图像又需要在不同的操作系统中显示,不同的操作系统在屏幕上显示像素矩阵有所差异,因此需要为不同的图像类再提供一组在不同操作系统显示的子类,如为 BMPImage 提供三个子类 BMPWindowsImp、BMPLinuxImp 和 BMPUnixImp,分别用于在 Windows、Linux 和 Unix 三个不同的操作系统下显示图像。

我们现在对该设计方案进行分析,发现存在如下两个主要问题:

(1)由于采用了多层继承结构,导致系统中类的个数急剧增加,图中,在各种图像的操作系统实现层提供了12个具体类,加上各级抽象层的类,系统中类的总个数达到了 17 个,在该设计方案中,具体层的类的个数 = 所支持的图像文件格式数×所支持的操作系统数。

(2)系统扩展麻烦,由于每一个具体类既包含图像文件格式信息,又包含操作系统信息,因此无论是增加新的图像文件格式还是增加新的操作系统,都需要增加大量的具体类,例如在图中增加一种新的图像文件格式 TIF,则需要增加 3 个具体类来实现该格式图像在3种不同操作系统的显示;如果增加一个新的操作系统 Mac OS,为了在该操作系统下能够显示各种类型的图像,需要增加 4 个具体类。这将导致系统变得非常庞大,增加运行和维护开销。

如何解决这两个问题?我们通过分析可得知,该系统存在两个独立变化的维度:图像文件格式和操作系统,如图所示:

enter image description here

在图中,如何将各种不同类型的图像文件解析为像素矩阵与图像文件格式本身相关,而如何在屏幕上显示像素矩阵则仅与操作系统相关。正因为图所示结构将这两种职责集中在一个类中,导致系统扩展麻烦,从类的设计角度分析,具体类 BMPWindowsImp、BMPLinuxImp 和 BMPUnixImp 等违反了“单一职责原则”,因为不止一个引起它们变化的原因,它们将图像文件解析和像素矩阵显示这两种完全不同的职责融合在一起,任意一个职责发生改变都需要修改它们,系统扩展困难。

如何改进?我们的方案是将图像文件格式(对应图像格式的解析)与操作系统(对应像素矩阵的显示)两个维度分离,使得它们可以独立变化,增加新的图像文件格式或者操作系统时都对另一个维度不造成任何影响。看到这里,大家可能会问,到底如何在软件中实现将两个维度分离呢?这里就可以使用 桥接模式.

处理多维度变化——桥接模式

概述

桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。

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

桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。

角色关系

桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥,桥接模式结构如图所示:

在桥接模式结构图中包含如下几个角色:

  • Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个 Implementor(实现类接口)类型的对象并可以维护该对象,它与 Implementor 之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。

  • RefinedAbstraction(扩充抽象类):扩充由 Abstraction 定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在 Abstraction 中声明的抽象业务方法,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。

  • Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor 接口仅提供基本操作,而 Abstraction 定义的接口可能会做更多更复杂的操作。Implementor 接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在 Abstraction 中不仅拥有自己的方法,还可以调用到 Implementor 中定义的方法,使用关联关系来替代继承关系。

  • ConcreteImplementor(具体实现类):具体实现 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现,在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给抽象类具体的业务操作方法。
    桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。

简单实现

在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图如图所示:

enter image description here

在图中,如果需要增加一种新型号的毛笔,只需扩展左侧的“抽象部分”,增加一个新的扩充抽象类;如果需要增加一种新的颜色,只需扩展右侧的“实现部分”,增加一个新的具体实现类。扩展非常方便,无须修改已有代码,且不会导致类的数目增长过快。

在具体编码实现时,由于在桥接模式中存在两个独立变化的维度,为了使两者之间耦合度降低,首先需要针对两个不同的维度提取抽象类和实现类接口,并建立一个抽象关联关系。对于“实现部分”维度,典型的实现类接口代码如下所示:

1
2
3
4
interface Implementor {
public void operationImpl();
}

在实现 Implementor 接口的子类中实现了在该接口中声明的方法,用于定义与该维度相对应的一些具体方法。
对于另一“抽象部分”维度而言,其典型的抽象类代码如下所示:

1
2
3
4
5
6
7
8
9
10
abstract class Abstraction {
protected Implementor impl; //定义实现类接口对象
public void setImpl(Implementor impl) {
this.impl=impl;
}
public abstract void operation(); //声明抽象业务方法
}

在抽象类 Abstraction 中定义了一个实现类接口类型的成员对象 impl,再通过注入的方式给该对象赋值,一般将该对象的可见性定义为 protected,以便在其子类中访问 Implementor 的方法,其子类一般称为扩充抽象类或细化抽象类(RefinedAbstraction),典型的 RefinedAbstraction 类代码如下所示:

1
2
3
4
5
6
7
8
class RefinedAbstraction extends Abstraction {
public void operation() {
//业务代码
impl.operationImpl(); //调用实现类的方法
//业务代码
}
}

对于客户端而言,可以针对两个维度的抽象层编程,在程序运行时再动态确定两个维度的子类,动态组合对象,将两个独立变化的维度完全解耦,以便能够灵活地扩充任一维度而对另一维度不造成任何影响。

适用范围

  1. 一个类存在两个或者以上的维度独立变化,并且这两个维度都需要扩展.
  2. 不希望使用继承或者因为多层次继承而导致整个系统类个数急剧增加的系统.
  3. 如果一个系统需要在构建的抽象画角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,可以通过桥接模式使它们在抽象层建立一个关联关系.
  4. 任何多维度之间的耦合都可以用桥接模式来解耦.

桥接模式在跨平台图片浏览系统中的应用

为了减少所需生成的子类数目,实现将操作系统和图像文件格式两个维度分离,使它们可以独立改变,Sunny 公司开发人员使用桥接模式来重构跨平台图像浏览系统的设计,其基本结构如图所示:

代码实现:

像素矩阵类

1
2
3
4
5
6
//像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
class Matrix {
//此处代码省略
}

抽象图像类(其中一个变化维度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 抽象图像类
*
* @author: fanyuzeng on 2018/1/4 14:46
*/
public abstract class Image {
protected IOperatorSystem os;
public void setOS(IOperatorSystem os) {
this.os = os;
}
public abstract void praseFileIntoImage(String filePath);
}

扩充图像类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* BMP格式图像扩充类
*
* @author: fanyuzeng on 2018/1/4 14:57
*/
public class BMPImage extends Image {
private static final String TAG = "==BMPImage==";
@Override
public void praseFileIntoImage(String filePath) {
//模拟创建图像矩阵
Matrix m = new Matrix(filePath);
//调用不同操作系统的展示图像方法去显示图片
os.showImage(m);
System.out.println("[praseFileIntoImage] " + filePath + ",格式为:BMP");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* GIF格式图像扩充类
*
* @author: fanyuzeng on 2018/1/4 14:58
*/
public class GIFImage extends Image {
private static final String TAG = "==GIFImage==";
@Override
public void praseFileIntoImage(String filePath) {
//模拟创建图像矩阵
Matrix m = new Matrix(filePath);
//调用不同操作系统的展示图像方法去显示图片
os.showImage(m);
System.out.println("[praseFileIntoImage] " + filePath + ",格式为:GIF");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* JPG格式图像扩充类
*
* @author: fanyuzeng on 2018/1/4 14:56
*/
public class JPGImage extends Image {
private static final String TAG = "==JPGImage==";
@Override
public void praseFileIntoImage(String filePath) {
//模拟创建图像矩阵
Matrix m = new Matrix(filePath);
//调用不同操作系统的展示图像方法去显示图片
os.showImage(m);
System.out.println("[praseFileIntoImage] " + filePath + "格式为:JPG");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* PNG格式图像扩充类
*
* @author: fanyuzeng on 2018/1/4 14:51
*/
public class PNGImage extends Image {
private static final String TAG = "==PNGImage==";
@Override
public void praseFileIntoImage(String filePath) {
//模拟创建图像矩阵
Matrix m = new Matrix(filePath);
//调用不同操作系统的展示图像方法去显示图片
os.showImage(m);
System.out.println("[praseFileIntoImage] " + filePath + ",格式为:PNG");
}
}

抽象操作系统接口(另外一个变化维度)

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author: fanyuzeng on 2018/1/4 14:48
*/
public interface IOperatorSystem {
/**
* 各个操作系统自带的将像素矩阵显示到屏幕上的方法
*
* @param m 像素矩阵
*/
void showImage(Matrix m);
}

具体操作系统实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Linux 操作系统
*
* @author: fanyuzeng on 2018/1/4 15:02
*/
public class LinuxOS implements IOperatorSystem {
private static final String TAG = "==LinuxOS==";
@Override
public void showImage(Matrix m) {
System.out.print("[showImage] " + "在Linux操作系统中显示像素矩阵 ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Unix 操作系统
*
* @author: fanyuzeng on 2018/1/4 15:02
*/
public class UnixOS implements IOperatorSystem {
private static final String TAG = "==UnixOS==";
@Override
public void showImage(Matrix m) {
System.out.print("[showImage] " + "在Unix操作系统中显示像素矩阵 ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* Window操作系统
* @author: fanyuzeng on 2018/1/4 15:01
*/
public class WindowsOS implements IOperatorSystem {
private static final String TAG = "==WindowsOS==";
@Override
public void showImage(Matrix m) {
System.out.print("[showImage] " + "在Windows操作系统中显示像素矩阵 ");
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test {
public static void main(String[] args) {
Image image1=new JPGImage();
image1.setOS(new WindowsOS());
image1.praseFileIntoImage("e:/pic/image1");
System.out.println("===============================");
Image image2=new GIFImage();
image2.setOS(new WindowsOS());
image2.praseFileIntoImage("e:/pic/image2");
System.out.println("===============================");
Image image3=new BMPImage();
image3.setOS(new LinuxOS());
image3.praseFileIntoImage("e:/pic/image3");
System.out.println("===============================");
Image image4=new PNGImage();
image4.setOS(new UnixOS());
image4.praseFileIntoImage("e:/pic/image4");
}
}

输出

1
2
3
4
5
6
7
8
[showImage] 在Windows操作系统中显示像素矩阵 [praseFileIntoImage] e:/pic/image1格式为:JPG
===============================
[showImage] 在Windows操作系统中显示像素矩阵 [praseFileIntoImage] e:/pic/image2,格式为:GIF
===============================
[showImage] 在Linux操作系统中显示像素矩阵 [praseFileIntoImage] e:/pic/image3,格式为:BMP
===============================
[showImage] 在Unix操作系统中显示像素矩阵 [praseFileIntoImage] e:/pic/image4,格式为:PNG

扩展

假设此时又需要支持一个 tiff 格式的图片,并且还要支持 MacOS,在这种模式下只需要继续在 Image 和 IOperatorSystem 下继续扩展和实现即可,大大提升了系统的可扩展性,降低了耦合性

Demo地址

https://github.com/zengfanyu/23DesignPatterns

共82.3k字
0%
.gt-container a{border-bottom: none;}