一. 什么是桥接模式?
桥接模式主要应对的是由于实际的需要,某个类具有两个或者两个以上的维度变化(违反了SRP原则),如果只是用继承将无法实现这种需要,或者使得设计变得相当臃肿。
桥接模式的目的: 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
二. 案例分析
我们以汽车为例, 汽车有各种品牌, 汽车需要发动机引擎才能开起来, 而引擎有各种型号. 现在就汽车和引擎进行组合. 比如: 我们有宝马, 奔驰, 引擎有2000cc, 2200cc. 如果我们想要拥有一辆可以开起来的汽车, 我们可以怎么组合呢?
有四种组合
- 宝马---2000cc
- 宝马---2200cc
- 奔驰---2000cc
- 奔驰---2200cc
加入再加一种车型呢?大众, 那就有多了两种组合 5. 大众---2000cc 6. 大众---2200cc
假如多了一种引擎呢? 2400cc, 那就再多出3种组合 7. 大众---2000cc 8. 宝马---2200cc 9. 奔驰---2400cc
他们的组合可能性是22, 或者23, 或者3*3.
继承方式实现
用程序怎么实现呢? 可以用继承. 第一步: 定义一个车类接口
/**
* 汽车
*/
public interface ICar {
public void run();
}
第二步: 定义宝马车, 实现车接口
/**
* 宝马
*/
public class BMW implements ICar {
@Override
public void run() {
}
}
定义奔驰车, 实现车接口
public class Benz implements ICar {
@Override
public void run() {
}
}
第三步: 定义宝马车使用2000cc的引擎
/**
* 宝马 -- 2000cc
*/
public class BMW2000cc implements ICar {
@Override
public void run() {
this.startEngine();
}
public void startEngine() {
System.out.println("启动发动机");
}
}
/**
* 宝马 -- 2200cc
*/
public class BMW2200cc implements ICar {
@Override
public void run() {
this.startEngine();
}
public void startEngine() {
System.out.println("启动发动机");
}
}
第四步: 奔驰汽车也是如此
/**
* 奔驰--2000cc
*/
public class Benz2000cc implements ICar {
@Override
public void run() {
this.startEngine();
}
public void startEngine() {
System.out.println("启动发动机");
}
}
/**
* 奔驰--2200cc
*/
public class Benz2200cc implements ICar {
@Override
public void run() {
this.startEngine();
}
public void startEngine() {
System.out.println("启动发动机");
}
}
这样就将形成了22=4种组合, 对应有4个类. 加入再多一种发动机或者多一种车型呢? 就变成23=6 或者 3*3=9, 对应有6或9个类. 类型越多类越多, 而且, 重复代码很多. 不方便扩展, 可复用性差
桥接模式实现
我们可以将车型和引擎分成两条线思考. 采用桥接模式, 我们来看看桥接模式的思路
这样做的好处, 可扩展性更好了, 比如, 要新增加一种车型, 大众, 只需要给车增加一个子类, 要增加一种引擎2400cc, 只需要给引擎增加一个子类, 不需要修改原来的代码. 到底是什么车型用什么引擎依然由客户端决定.
下面来看具体代码实现:
第一步: 车的接口
/**
* 汽车
*/
public interface ICar {
public void run();
}
奔驰和宝马都是车的一种.
public class Benz implements ICar {
@Override
public void run() {
}
}
/**
* 宝马
*/
public class BMW implements ICar {
@Override
public void run() {
}
}
第二步: 引擎的接口
/**
* 引擎
*/
public interface IEngine {
/**
* 启动引擎
*/
void startEngine();
}
2000cc和2200cc都是引擎的一种类型
public class Engine2200cc implements IEngine{
@Override
public void startEngine() {
System.out.println("发动 2200cc 的引擎");
}
}
public class Engine2000cc implements IEngine{
@Override
public void startEngine() {
System.out.println("发动 2000cc 的引擎");
}
}
第三步: 构建引擎和车的关系---改造接口 引擎是车的组成部分. 这时我们可以在抽象车中引用引擎, 具体车中增加构造引擎类型的入口
/**
* 汽车
*/
public abstract class ICar {
IEngine iEngine;
public ICar(IEngine iEngine) {
this.iEngine = iEngine;
}
public abstract void run();
}
public class Benz extends ICar {
public Benz(IEngine iEngine) {
super(iEngine);
}
@Override
public void run() {
}
}
public class BMW extends ICar {
public BMW(IEngine iEngine) {
super(iEngine);
}
@Override
public void run() {
}
}
这样, 客户端在调用的时候, 到底相配什么车型, 什么引擎, 就有客户端说了算了. 第三步: 客户端调用
public class Client {
public static void main(String[] args) {
IEngine iEngine2000cc = new Engine2000cc();
IEngine iEngine2200cc = new Engine2200cc();
ICar benz = new Benz(iEngine2000cc);
System.out.println("奔驰跑起来了");
benz.run();
System.out.println();
ICar bmw = new BMW(iEngine2200cc);
System.out.println("宝马跑起来了");
bmw.run();
}
}
运行结果:
奔驰跑起来了
发动2000cc的引擎
宝马跑起来了
发动2200cc的引擎
三. 桥接模式总结
- 桥接模式使用对象间的组合关系解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。
- 所谓抽象和实现沿着各自维度的变化,即“子类化”它们,得到各个子类之后,便可以任由它们组合,从而获得不同的组合结果。
- 桥接模式有时候类似于多继承方案,但是多继承方案往往违背了SRP原则,复用性较差。桥接模式是比继承更好的解决方法。
- 桥接模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换言之两个变化不会导致纵横交错的结果,不一定要使用桥接模式。
桥接模式就像坐标轴的两个坐标, 横轴是车型, 纵轴是引擎型号. 然后有多少种匹配方式以及怎么匹配由客户端决定.如下图所示:
如上图, 到底怎么组合由客户端决定, 且可随意组合.
三. 桥接模式的优缺点
优点: 1、抽象和实现的分离; 2、优秀的扩展能力; 3、实现细节对客户透明。
缺点: 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
四. 桥接模式的使用场景
1、一个类存在两个变化的维度,而且这两个维度都有可能会扩展。对于这种有两个独立变化的维度,使用桥接模式再适合不过了。 2、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,采用继承的方案, 会产生很多子类, 增加系统类的个数和复杂度. 如果不希望产生过多的子类, 可以考虑使用桥接模式。分析功能变化的原因,看是否可以拆分成不同的纬度,然后通过桥接模式来解耦它们,从而减少子类的个数。
- 如果你不想在抽象和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象和实现分开,客户是面向抽象的接口编程,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,也可以说对客户是透明的,还可以动态切换具体实现。