师说
桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。
桥接模式和适配器模式的区别在于使用场合不同,适配器模式主要解决两个已有接口间的匹配问题。这种情况下被适配的接口的实现往往是一个黑匣子。我们不想,也不能改变这个接口及其实现。同时也不能控制其演化,只要相关的对象能与系统定义的接口协同工作即可。适配器模式经常用在与第三方产品的功能集成上,采用该模式适应新类型的增加的方式是开发针对这个类的适配器。而桥接模式则不同,参与桥接的接口是稳定的,用户可以扩展和修改桥接种的类,但是不能改变接口。桥接模式通过接口继承或者类继承实现功能的扩展。
按照GOF的说法,桥接模式和适配器模式用于设计的不同阶段,桥接模式用于设计的前期,即在设计类的时候将类规划为逻辑和实现两大类,使他们可以分别进行演化;而适配器模式用于设计完成之后,当发现完成的类无法协同工作时,可以采用适配器模式。如下图的报表处理模块所示:
也可以看我大三以前的上这门课时的博客:
之前我们学习了 5 种构建型模式。它们主要用于构建对象。我们简单回顾一下:
- 工厂方法模式:为每一类对象建立工厂,将对象交由工厂创建,客户端只和工厂打交道。
- 抽象工厂模式:为每一类工厂提取出抽象接口,使得新增工厂、替换工厂变得非常容易。
- 建造者模式:用于创建构造过程稳定的对象,不同的 Builder 可以定义不同的配置。
- 单例模式:全局使用同一个对象,分为饿汉式和懒汉式。懒汉式有双检锁和内部类两种实现方式。
- 原型模式:为一个类定义 clone 方法,使得创建相同的对象更方便。
本篇文章我们将一起学习结构型模式,顾名思义,结构型模式是用来设计程序的结构的。结构型模式就像搭积木,将不同的类结合在一起形成契合的结构。包括以下几种:
- 适配器模式
- 桥接模式
- 组合模式
- 装饰模式
- 外观模式
- 享元模式
- 代理模式
由于内容较多,本篇我们先讲解前两种模式。
适配器模式
说到适配器,我们最熟悉的莫过于电源适配器了,也就是手机的充电头。它就是适配器模式的一个应用。
试想一下,你有一条连接电脑和手机的 USB 数据线,连接电脑的一端从电脑接口处接收 5V 的电压,连接手机的一端向手机输出 5V 的电压,并且他们工作良好。
中国的家用电压都是 220V,所以 USB 数据线不能直接拿来给手机充电,这时候我们有两种方案:
- 单独制作手机充电器,接收 220V 家用电压,输出 5V 电压。
- 添加一个适配器,将 220V 家庭电压转化为类似电脑接口的 5V 电压,再连接数据线给手机充电。
如果你使用过早期的手机,就会知道以前的手机厂商采用的就是第一种方案:早期的手机充电器都是单独制作的,充电头和充电线是连在一起的。现在的手机都采用了电源适配器加数据线的方案。这是生活中应用适配器模式的一个进步。
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配的意思是适应、匹配。通俗地讲,适配器模式适用于有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。
家用电源和 USB 数据线有相关性:家用电源输出电压,USB 数据线输入电压。但两个接口无法兼容,因为一个输出 220V,一个输入 5V,通过适配器将输出 220V 转换成输出 5V 之后才可以一起工作。
让我们用程序来模拟一下这个过程。
首先,家庭电源提供 220V 的电压:
Java 实现
1 | class HomeBattery { |
USB 数据线只接收 5V 的充电电压:
1 | class USBLine { |
先来看看适配之前,用户如果直接用家庭电源给手机充电:
1 | public class User { |
运行程序,输出如下:
1 | 家庭电源提供的电压是 220V |
这时,我们加入电源适配器:
1 | class Adapter { |
然后,用户再使用适配器将家庭电源提供的电压转换为充电电压:
1 | public class User { |
运行程序,输出如下:
1 | 家庭电源提供的电压是 220V |
这就是适配器模式。在我们日常的开发中经常会使用到各种各样的 Adapter,都属于适配器模式的应用。
但适配器模式并不推荐多用。因为未雨绸缪好过亡羊补牢,如果事先能预防接口不同的问题,不匹配问题就不会发生,只有遇到源接口无法改变时,才应该考虑使用适配器。比如现代的电源插口中很多已经增加了专门的充电接口,让我们不需要再使用适配器转换接口,这又是社会的一个进步。
桥接模式
考虑这样一个需求:绘制矩形、圆形、三角形这三种图案。按照面向对象的理念,我们至少需要三个具体类,对应三种不同的图形。
抽象接口 IShape:
1 | public interface IShape { |
三个具体形状类:
1 | class Rectangle implements IShape { |
1 | class Round implements IShape { |
1 | class Triangle implements IShape { |
接下来我们有了新的需求,每种形状都需要有四种不同的颜色:红、蓝、黄、绿。
这时我们很容易想到两种设计方案:
- 为了复用形状类,将每种形状定义为父类,每种不同颜色的图形继承自其形状父类。此时一共有 12 个类。
- 为了复用颜色类,将每种颜色定义为父类,每种不同颜色的图形继承自其颜色父类。此时一共有 12 个类。
乍一看没什么问题,我们使用了面向对象的继承特性,复用了父类的代码并扩展了新的功能。
但仔细想一想,如果以后要增加一种颜色,比如黑色,那么我们就需要增加三个类;如果再要增加一种形状,我们又需要增加五个类,对应 5 种颜色。
更不用说遇到增加 20 个形状,20 种颜色的需求,不同的排列组合将会使工作量变得无比的庞大。看来我们不得不重新思考设计方案。
形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。
官方定义非常精准、简练,但却有点不易理解。通俗地说,如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化,比如本例中的形状和颜色。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。
说到这里,不得不提一个设计原则:合成 / 聚合复用原则。虽然它没有被划分到六大设计原则中,但它在面向对象的设计中也非常的重要。
合成 / 聚合复用原则:优先使用合成 / 聚合,而不是类继承。
继承虽然是面向对象的三大特性之一,但继承会导致子类与父类有非常紧密的依赖关系,它会限制子类的灵活性和子类的复用性。而使用合成 / 聚合,也就是使用接口实现的方式,就不存在依赖问题,一个类可以实现多个接口,可以很方便地拓展功能。
让我们一起来看一下本例使用桥接模式的程序实现:
新建接口类 IColor,仅包含一个获取颜色的方法:
1 | public interface IColor { |
每种颜色都实现此接口:
1 | public class Red implements IColor { |
1 | public class Blue implements IColor { |
1 | public class Yellow implements IColor { |
1 | public class Green implements IColor { |
在每个形状类中,桥接 IColor 接口:
1 | class Rectangle implements IShape { |
1 | class Round implements IShape { |
1 | class Triangle implements IShape { |
测试函数:
1 |
|
运行程序,输出如下:
1 | 绘制红矩形 |
这时我们再来回顾一下官方定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。抽象部分指的是父类,对应本例中的形状类,实现部分指的是不同子类的区别之处。将子类的区别方式 —— 也就是本例中的颜色 —— 分离成接口,通过组合的方式桥接颜色和形状,这就是桥接模式,它主要用于两个或多个同等级的接口。
总结
到这里我们就把结构型模式的前两种介绍完了,让我们总结一下:
- 适配器模式:用于有相关性但不兼容的接口
- 桥接模式:用于同等级的接口互相组合