设计模式之【桥接模式】


设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

以心法为基础,以武器运用招式应对复杂的编程问题。

在现实生活中,某些类具有两个或多个维度的变化,比如,图形既可以按形状分,又可以按颜色分;手机既可以按品牌分,又可以按配置分;还有支持不同平台和不同文件格式的视频播放器等等。你看,如果两个维度上分别有m,n种类型,那么,就会有m*n种组合,如果有多个维度上的变化,那么会有更多种组合。如果使用继承方式,不但对应的子类会非常多,而且会导致扩展和修改都很困难。

这时候,桥接(桥梁)模式就派上用场啦~

桥接模式

将抽象和实现解耦,使得两者可以独立地变化。

抽象类或接口和实现解耦?独立地变化?what are you saying?不应该是“抽象”不变,“实现”可以随便改变嘛?

其实,桥接模式中有两种角色,分别是抽象化角色和实现化角色,实现化角色并不是直接实现了抽象化角色定义的接口,而是提供更底层的方法,使抽象化角色可以基于实现化角色封装出自己的接口实现。

所谓“解耦”, 我们知道,桥,用来将河的两岸连通起来,而我们这里的桥梁模式,就是用来将两个独立的结构联系起来,这两个被联系起来的结构可以独立的变化。

桥梁模式的重点是在“解耦”上。

比如,我们要画一个有颜色、有形状的图形,那么说明这个具体的图形在形状和颜色上都会变化的,如下图所示:

如果我们按形状来分类,那么它的结构图如下:

具体代码如下:

现有长方形和正方形,它们可以抽象出形状这个父类,然后它们对应两个子类。

 1 abstract class Shape {
 2     abstract void draw();
 3 }
 4 ?
 5 public class Rectangle extends Shape {
 6     @Override void draw() { System.out.println("绘制长方形"); }
 7 }
 8 ?
 9 public class Square extends Shape {
10     @Override void draw() { System.out.println("绘制正方形"); }
11 }

如果加入了颜色,有红色和蓝色,跟形状组合成下面4种子类:

 1 public class RedRectangle extends Rectangle {
 2     @Override void draw() { System.out.println("绘制红色长方形"); }
 3 }
 4 ?
 5 public class RedSquare extends Square {
 6     @Override void draw() { System.out.println("绘制红色正方形"); }
 7 }
 8 ?
 9 public class BlueRectangle extends Rectangle {
10     @Override void draw() { System.out.println("绘制蓝色长方形"); }
11 }
12 ?
13 public class BlueSquare extends Square {
14     @Override void draw() { System.out.println("绘制蓝色正方形"); }
15 }

你看,现在有两种形状,分别是长方形和正方形。同时也有两种颜色,那么就会有2 * 2种组合,假如未来有一天,我们要新增一个圆形,那么就会有3 * 2种组合,类的个数暴涨。或者如果不要现在不要长方形了,但是要圆形,那么,改了“长方形”类,它的子类“红色长方形”和“蓝色长方形”都要跟着改。这完全违背了

那么,如果按颜色分类会怎样呢?我们来看一下实现的结构图:

你看,这个结构体系还是一样的,不管是按形状还是按颜色分类,整个继承体系都会很复杂,很庞大,牵一发而动全身。

现在,我们这个业务场景有两个角度分类,每个角度都有可能变化,那么就把这种多角度分离出来(使用关联关系),让它们独立变化,减少它们之间的耦合。这就是“桥接模式”,我们先来认识一下桥接模式的结构图是怎样的:

设计的角色:

  • Abstraction:定义抽象接口,拥有一个Implementor类型的对象引用;

  • RefinedAbstraction:扩展Abstraction中的接口定义;

  • Implementor:是具体实现的接口,Implementor和RefinedAbstraction接口并不一定完全一致,实际上这两个接口可以完全不一样,Implementor提供具体操作方法,而Abstraction提供更高层次的调用;

  • ConcreteImplementor:实现Implementor接口,给出具体实现。

好啦,我们回到画带有颜色的图形的例子中,看看使用桥接模式后,它的结构图是怎样的:

你看,是不是清晰很多了,左右两边各有一块“大陆”,中间使用一座“桥梁”,把它们联系起来。未来,在每块“大陆”内部不管做删减、还是新增,都不会影响另一块“大陆”的建设。这不就是“组合优于继承”的好处嘛。

我们来看一下代码实现:

Implementor(这里是形状的接口)

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

ConcreteImplementor(具体的形状:长方形和正方形)

 1 public class Square implements Shape {
 2     @Override 
 3     public void draw() {
 4         System.out.println("绘制正方形");
 5     }
 6 }
 7 ?
 8 public class Rectangle implements Shape {
 9     @Override 
10     public void draw() {
11         System.out.println("绘制长方形");
12     }
13 }
14 ?
15 // 新增一个圆形,逻辑是一样的,直接新增一个实现类...很灵活

Abstraction(颜色抽象类,这里包含了一个Implementor)

 1 public abstract class Color {
 2     // 组合Shape接口
 3     protected Shape shape;
 4     
 5     public Color(Shape shape) {
 6         this.shape = shape;
 7     }
 8     
 9     public abstract void coloring();
10 }

RefinedAbstraction(具体的颜色类)

 1 public class BlueShape extends Color {
 2     public BlueShape(Shape shape) {
 3         super(shape);
 4     }
 5     
 6     @Override
 7     public void coloring() {
 8         super.shape.draw();
 9         System.out.println("给该图形染上蓝色");
10     }
11 }
12 ?
13 public class RedShape extends Color {
14     public RedShape(Shape shape) {
15         super(shape);
16     }
17     
18     @Override
19     public void coloring() {
20         super.shape.draw();
21         System.out.println("给该图形染上红色");
22     }
23 }

现在,我们来画一个蓝色的正方形:

1 Shape square = new Square();
2 Color blueSquare = new BlueShape(square);
3 blueSquare.coloring();

如果你要画红色的长方形,也是一样的逻辑:

1 Shape rectangle = new Rectangle();
2 Color redRectangle = new BlueShape(rectangle);
3 redRectangle.coloring();

继承一定“一无是处”吗?

我们知道,继承是强侵入的,父类有一个方法,子类也必须有这个方法,这是不可选择的,这样就会带来扩展性的问题。比如,Father类有一个方法A,Son继承了这个方法,然后GrandSon也继承了这个方法。那么如果,未来有一天,Son要重写Father的这个方法,可以随便重写嘛?肯定不能的,因为GrandSon还在用从Father继承过来的方法A,如果Son重写了,那就要修改Son和GrandSon之间的关系,这样的改动,风险就太大了。

但并不是说继承并不好,只是我们需要根据具体的应用场景,具体分析。比如,对于比较明确不发生变化的,或者确定是“is-a”的关系时,就通过继承来完成;若不能确定是否会发生变化的,那就认为是会发生变化,则考虑使用桥梁模式来解决。具体问题,具体分析!

桥梁模式的优点

  • 抽象和实现分离

    这也是桥梁模式的主要特点,它完全是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。

  • 优秀的扩充能力

    想增加实现?没问题,不改变已有代码!想增加抽象?也没问题,也不改变已有代码!只要对外暴露的接口层允许这样的变化,我们已经把变化的可能性减到最小。

  • 实现细节对客户透明

    客户不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装。

桥梁模式的缺点

  • 增加了系统的理解和设计难度。

  • 需要正确地识别出系统中两个或多个独立变化的维度。

桥梁模式的应用场景

  • 不希望或不适用使用继承的场景

    例如,继承层次过度、无法更细化设计颗粒度等场景,需要考虑用桥梁模式。

  • 接口或抽象类不稳定的场景

    明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的,也是比较失败的做法。

  • 重用性要求较高的场景

    设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。

总结

桥接模式使用对象间的组合关系解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。

参考

《设计模式之禅》

https://www.runoob.com/w3cnote/bridge-pattern2.html

https://honeypps.com/design_pattern/bridge/