在正式介绍桥接模式之前,我先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色,如果使用蜡笔,需要准备3×12 = 36支,但如果使用毛笔的话,只需要提供3种型号的毛笔,外加12个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与36支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有12种颜色,对应的蜡笔需增加12支,而毛笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况,即本章将要介绍的桥接模式。
10.1 跨平台图像浏览系统
Sunny软件公司欲开发一个跨平台图像浏览系统,要求该系统能够显示BMP、JPG、GIF、PNG等多种格式的文件,并且能够在Windows、Linux、Unix等多个操作系统上运行。系统首先将各种格式的文件解析为像素矩阵(Matrix),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。系统需具有较好的扩展性以支持新的文件格式和操作系统。
Sunny软件公司的开发人员针对上述要求,提出了一个初始设计方案,其基本结构如图10-1所示:
在图10-1的初始设计方案中,使用了一种多层继承结构,Image是抽象父类,而每一种类型的图像类,如BMPImage、JPGImage等作为其直接子类,不同的图像文件格式具有不同的解析方法,可以得到不同的像素矩阵;由于每一种图像又需要在不同的操作系统中显示,不同的操作系统在屏幕上显示像素矩阵有所差异,因此需要为不同的图像类再提供一组在不同操作系统显示的子类,如为BMPImage提供三个子类BMPWindowsImp、BMPLinuxImp和BMPUnixImp,分别用于在Windows、Linux和Unix三个不同的操作系统下显示图像。
我们现在对该设计方案进行分析,发现存在如下两个主要问题:
(1)由于采用了多层继承结构,导致系统中类的个数急剧增加,图10-1中,在各种图像的操作系统实现层提供了12个具体类,加上各级抽象层的类,系统中类的总个数达到了17个,在该设计方案中,具体层的类的个数 = 所支持的图像文件格式数×所支持的操作系统数。
(2)系统扩展麻烦,由于每一个具体类既包含图像文件格式信息,又包含操作系统信息,因此无论是增加新的图像文件格式还是增加新的操作系统,都需要增加大量的具体类,例如在图10-1中增加一种新的图像文件格式TIF,则需要增加3个具体类来实现该格式图像在3种不同操作系统的显示;如果增加一个新的操作系统Mac OS,为了在该操作系统下能够显示各种类型的图像,需要增加4个具体类。这将导致系统变得非常庞大,增加运行和维护开销。
如何解决这两个问题?我们通过分析可得知,该系统存在两个独立变化的维度:图像文件格式和操作系统,如图10-2所示:
在图10-2中,如何将各种不同类型的图像文件解析为像素矩阵与图像文件格式本身相关,而如何在屏幕上显示像素矩阵则仅与操作系统相关。正因为图10-1所示结构将这两种职责集中在一个类中,导致系统扩展麻烦,从类的设计角度分析,具体类BMPWindowsImp、BMPLinuxImp和BMPUnixImp等违反了“单一职责原则”,因为不止一个引起它们变化的原因,它们将图像文件解析和像素矩阵显示这两种完全不同的职责融合在一起,任意一个职责发生改变都需要修改它们,系统扩展困难。
如何改进?我们的方案是将图像文件格式(对应图像格式的解析)与操作系统(对应像素矩阵的显示)两个维度分离,使得它们可以独立变化,增加新的图像文件格式或者操作系统时都对另一个维度不造成任何影响。看到这里,大家可能会问,到底如何在软件中实现将两个维度分离呢?不用着急,本章我将为大家详细介绍一种用于处理多维度变化的设计模式——桥接模式。