aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

设计模式-20软工第5周安排-结构型模式-适配器模式、桥接模式

师说

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

桥接模式和适配器模式的区别在于使用场合不同,适配器模式主要解决两个已有接口间的匹配问题。这种情况下被适配的接口的实现往往是一个黑匣子。我们不想,也不能改变这个接口及其实现。同时也不能控制其演化,只要相关的对象能与系统定义的接口协同工作即可。适配器模式经常用在与第三方产品的功能集成上,采用该模式适应新类型的增加的方式是开发针对这个类的适配器。而桥接模式则不同,参与桥接的接口是稳定的,用户可以扩展和修改桥接种的类,但是不能改变接口。桥接模式通过接口继承或者类继承实现功能的扩展。

按照GOF的说法,桥接模式和适配器模式用于设计的不同阶段,桥接模式用于设计的前期,即在设计类的时候将类规划为逻辑和实现两大类,使他们可以分别进行演化;而适配器模式用于设计完成之后,当发现完成的类无法协同工作时,可以采用适配器模式。如下图的报表处理模块所示:


也可以看我大三以前的上这门课时的博客:

设计模式-结构型模式-结构型模式概述
设计模式-结构型模式-适配器模式
设计模式-结构型模式-桥接模式


之前我们学习了 5 种构建型模式。它们主要用于构建对象。我们简单回顾一下:

  • ​工厂方法模式:为每一类对象建立工厂,将对象交由工厂创建,客户端只和工厂打交道。
  • 抽象工厂模式:为每一类工厂提取出抽象接口,使得新增工厂、替换工厂变得非常容易。
  • 建造者模式:用于创建构造过程稳定的对象,不同的 Builder 可以定义不同的配置。
  • 单例模式:全局使用同一个对象,分为饿汉式和懒汉式。懒汉式有双检锁和内部类两种实现方式。
  • 原型模式:为一个类定义 clone 方法,使得创建相同的对象更方便。

本篇文章我们将一起学习结构型模式,顾名思义,结构型模式是用来设计程序的结构的。结构型模式就像搭积木,将不同的类结合在一起形成契合的结构。包括以下几种:

  • 适配器模式
  • 桥接模式
  • 组合模式
  • 装饰模式
  • 外观模式
  • 享元模式
  • 代理模式

由于内容较多,本篇我们先讲解前两种模式


适配器模式

说到适配器,我们最熟悉的莫过于电源适配器了,也就是手机的充电头。它就是适配器模式的一个应用。

试想一下,你有一条连接电脑和手机的 USB 数据线,连接电脑的一端从电脑接口处接收 5V 的电压,连接手机的一端向手机输出 5V 的电压,并且他们工作良好。

中国的家用电压都是 220V,所以 USB 数据线不能直接拿来给手机充电,这时候我们有两种方案:

  • 单独制作手机充电器,接收 220V 家用电压,输出 5V 电压。
  • 添加一个适配器,将 220V 家庭电压转化为类似电脑接口的 5V 电压,再连接数据线给手机充电。

如果你使用过早期的手机,就会知道以前的手机厂商采用的就是第一种方案:早期的手机充电器都是单独制作的,充电头和充电线是连在一起的。现在的手机都采用了电源适配器加数据线的方案。这是生活中应用适配器模式的一个进步。

适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

适配的意思是适应、匹配。通俗地讲,适配器模式适用于有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。

家用电源和 USB 数据线有相关性:家用电源输出电压,USB 数据线输入电压。但两个接口无法兼容,因为一个输出 220V,一个输入 5V,通过适配器将输出 220V 转换成输出 5V 之后才可以一起工作。

让我们用程序来模拟一下这个过程。

首先,家庭电源提供 220V 的电压:

Java 实现

1
2
3
4
5
6
class HomeBattery {
int supply() {
// 家用电源提供一个 220V 的输出电压
return 220;
}
}

USB 数据线只接收 5V 的充电电压:

1
2
3
4
5
6
7
8
class USBLine {
void charge(int volt) {
// 如果电压不是 5V,抛出异常
if (volt != 5) throw new IllegalArgumentException("只能接收 5V 电压");
// 如果电压是 5V,正常充电
System.out.println("正常充电");
}
}

先来看看适配之前,用户如果直接用家庭电源给手机充电:

1
2
3
4
5
6
7
8
9
10
11
public class User {
@Test
public void chargeForPhone() {
HomeBattery homeBattery = new HomeBattery();
int homeVolt = homeBattery.supply();
System.out.println("家庭电源提供的电压是 " + homeVolt + "V");

USBLine usbLine = new USBLine();
usbLine.charge(homeVolt);
}
}

运行程序,输出如下:

1
2
家庭电源提供的电压是 220V
java.lang.IllegalArgumentException: 只能接收 5V 电压

这时,我们加入电源适配器:

1
2
3
4
5
6
7
class Adapter {
int convert(int homeVolt) {
// 适配过程:使用电阻、电容等器件将其降低为输出 5V
int chargeVolt = homeVolt - 215;
return chargeVolt;
}
}

然后,用户再使用适配器将家庭电源提供的电压转换为充电电压:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
@Test
public void chargeForPhone() {
HomeBattery homeBattery = new HomeBattery();
int homeVolt = homeBattery.supply();
System.out.println("家庭电源提供的电压是 " + homeVolt + "V");

Adapter adapter = new Adapter();
int chargeVolt = adapter.convert(homeVolt);
System.out.println("使用适配器将家庭电压转换成了 " + chargeVolt + "V");

USBLine usbLine = new USBLine();
usbLine.charge(chargeVolt);
}
}

运行程序,输出如下:

1
2
3
家庭电源提供的电压是 220V
使用适配器将家庭电压转换成了 5V
正常充电

这就是适配器模式。在我们日常的开发中经常会使用到各种各样的 Adapter,都属于适配器模式的应用。

但适配器模式并不推荐多用。因为未雨绸缪好过亡羊补牢,如果事先能预防接口不同的问题,不匹配问题就不会发生,只有遇到源接口无法改变时,才应该考虑使用适配器。比如现代的电源插口中很多已经增加了专门的充电接口,让我们不需要再使用适配器转换接口,这又是社会的一个进步。


桥接模式

考虑这样一个需求:绘制矩形、圆形、三角形这三种图案。按照面向对象的理念,我们至少需要三个具体类,对应三种不同的图形。

抽象接口 IShape:

1
2
3
public interface IShape {
void draw();
}

三个具体形状类:

1
2
3
4
5
6
class Rectangle implements IShape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
1
2
3
4
5
6
class Round implements IShape {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
1
2
3
4
5
6
class Triangle implements IShape {
@Override
public void draw() {
System.out.println("绘制三角形");
}
}

接下来我们有了新的需求,每种形状都需要有四种不同的颜色:红、蓝、黄、绿。

这时我们很容易想到两种设计方案:

  • 为了复用形状类,将每种形状定义为父类,每种不同颜色的图形继承自其形状父类。此时一共有 12 个类。
  • 为了复用颜色类,将每种颜色定义为父类,每种不同颜色的图形继承自其颜色父类。此时一共有 12 个类。

乍一看没什么问题,我们使用了面向对象的继承特性,复用了父类的代码并扩展了新的功能。

但仔细想一想,如果以后要增加一种颜色,比如黑色,那么我们就需要增加三个类;如果再要增加一种形状,我们又需要增加五个类,对应 5 种颜色。

更不用说遇到增加 20 个形状,20 种颜色的需求,不同的排列组合将会使工作量变得无比的庞大。看来我们不得不重新思考设计方案。

形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。

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

官方定义非常精准、简练,但却有点不易理解。通俗地说,如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化,比如本例中的形状和颜色。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。

说到这里,不得不提一个设计原则:合成 / 聚合复用原则。虽然它没有被划分到六大设计原则中,但它在面向对象的设计中也非常的重要。

合成 / 聚合复用原则:优先使用合成 / 聚合,而不是类继承。

继承虽然是面向对象的三大特性之一,但继承会导致子类与父类有非常紧密的依赖关系,它会限制子类的灵活性和子类的复用性。而使用合成 / 聚合,也就是使用接口实现的方式,就不存在依赖问题,一个类可以实现多个接口,可以很方便地拓展功能。

让我们一起来看一下本例使用桥接模式的程序实现:

新建接口类 IColor,仅包含一个获取颜色的方法:

1
2
3
public interface IColor {
String getColor();
}

每种颜色都实现此接口:

1
2
3
4
5
6
public class Red implements IColor {
@Override
public String getColor() {
return "红";
}
}
1
2
3
4
5
6
public class Blue implements IColor {
@Override
public String getColor() {
return "蓝";
}
}
1
2
3
4
5
6
public class Yellow implements IColor {
@Override
public String getColor() {
return "黄";
}
}
1
2
3
4
5
6
public class Green implements IColor {
@Override
public String getColor() {
return "绿";
}
}

在每个形状类中,桥接 IColor 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle implements IShape {

private IColor color;

void setColor(IColor color) {
this.color = color;
}

@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "矩形");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Round implements IShape {

private IColor color;

void setColor(IColor color) {
this.color = color;
}

@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "圆形");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Triangle implements IShape {

private IColor color;

void setColor(IColor color) {
this.color = color;
}

@Override
public void draw() {
System.out.println("绘制" + color.getColor() + "三角形");
}
}

测试函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void drawTest() {
Rectangle rectangle = new Rectangle();
rectangle.setColor(new Red());
rectangle.draw();

Round round = new Round();
round.setColor(new Blue());
round.draw();

Triangle triangle = new Triangle();
triangle.setColor(new Yellow());
triangle.draw();
}

运行程序,输出如下:

1
2
3
绘制红矩形
绘制蓝圆形
绘制黄三角形

这时我们再来回顾一下官方定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。抽象部分指的是父类,对应本例中的形状类,实现部分指的是不同子类的区别之处。将子类的区别方式 —— 也就是本例中的颜色 —— 分离成接口,通过组合的方式桥接颜色和形状,这就是桥接模式,它主要用于两个或多个同等级的接口


总结

到这里我们就把结构型模式的前两种介绍完了,让我们总结一下:

  • 适配器模式:用于有相关性但不兼容的接口
  • 桥接模式:用于同等级的接口互相组合

参考资料

20软工第5周安排
详解设计模式之结构型模式(上)