相思相见知何日 此时此夜难为情
——李白《秋风词》
将抽象的部分和实现的部分分离,使他们可以单独的变化
在之前我们聊过的设计模式中,我们会说A类是一个工厂类,B类是一个适配器;但是在桥接中,绝对不会出现某个类是一个桥接类的情况。
桥接是个动词,他是在描述一种行为,那就是在
两个不同类簇内的两个具体类之间建立连接
,而且这种连接是可以在程序运行的过程中动态调整的。
上面这段话现在看不明白没关系,这也是我讨厌讲定义的原因,多直白的定义其实都没有一个例子来得直接
所以准备好了吗?这次的例子开始了:
假定我们现在有这样的需求:
我们有 A/B/C 三种型号的热水壶,每一种型号的热水壶都有:开机,关机,加热 的功能
但是每一种型号的热水壶加热模组的底层实现各不相同
- A型号用传统的发热盘加热
- B型号用微波方式加热
- C型号用最新的量子缠绕加热(这个先进到现在还没研究出来)
根据这样的业务,我们定义出了这样的热水壶类簇:
并不是所有顾客都在意我们的热水壶到底是用加热盘还是用量子进行加热的,所以我们需要新的产品线来给我们的产品分层,于是新的业务来了:
也就是说,我们拓展了热水壶类型X和Y,而他们具体的加热方式可以是加热盘、微波或者量子缠绕。我们现在的产品线就像这样:
要展示这样的一个产品树,很容易想到使用继承来实现,就像这样:
我在想,如果这几天做噩梦,那个梦里一定会有这个UML的身影
由于 A/B/C 和 X/Y 可以随意组合,所以每次因为新的业务对这个类簇进行改动都会是成倍的增长子类数量,这毫无疑问是一场灾难
像这种在一个模块中出现大量意义极小的子类的情况,我们将其称之为类爆炸
类爆炸
指在实现一个功能的时候,本来不需要这么多的类,但是却设计了那么多,使得维护成本过高的问题
问题出在继承机制让所有 Kettle 的子类的代码都和 “平台”
存在了关联
当我们创建一个 Kettle 对象的时候,我们必须要指定他到底是用 A/B/C哪个型号的电机
继承机制让 X/Y 和 A/B/C 出现了紧耦合,我们只能通过硬编码来表达他们的组合方式
这就是上文的例子给你的别扭感的根源,你所建的新子类只不过是N个排列组合中的其中一种,而没有什么实质的效益
那有没有可能实现解耦呢?
于是乎,我们整理一下Kettle的产品线,就像这样:
我们发现 A/B/C 和 X/Y 其实是不同层级的接口
X/Y中的特有的接口其实都是通过调用 A/B/C 中的接口实现的。也就是说,其实X/Y是比A/B/C更高级的抽象
所以第一步,我们先把他们拆成两个不同的类树,就像这样:
到这里,Kettle类树专门用来负责拓展功能,Machine类树专门用来拓展电机
至此,功能和电机之间的耦合被解除
但是我们发现如果 Kettle 没有 Machine(电机),其实他什么都做不了,所以他们之间必须存在某种关联,就像这样:
public class Kettle{
private Machine machine;
public Kettle(Machine machine){
this.machine = machine;
}
public void setMachine(Machine machine){
this.machine = machine;
}
public Machine getMachine(){
return machine;
}
public void turnOn(){
machine.turnOn();
}
public void heatUpWater(){
while(machine.getTemperature() < 100){
machine.warmUp();
}
}
public void warmUp(){
machine.warmUp();
}
public float getTemperature(){
return machine.getTemperature();
}
}
public class Kettle_X extends Kettle{
//保温功能 这个方法必须在子线程中被调用
public void insulation() throws InterruptedException{
while(true){
if(getTemperature() < 70){//直接调用父类的方法
heatUpWater();
}
sleep(5000);
}
}
}
public class Kettle_Y extends Kettle{
//乌龙茶
public void wulongTea(){
while(getTemperature() < 75){
warmUp();
}
}
}
public interface Machine{
void turnOn();
void warmUp();
void turnOff();
float getTemperature();
}
public class Machine_A implements Machine{
public void turnOn(){
通电
}
public void warmUp(){
加热发热盘
}
public void turnOff(){
断电
}
public float getTemperature(){
获取传感器的温度
}
}
public class Machine_B implements Machine{
public void turnOn(){
通电
}
public void warmUp(){
微波发射
}
public void turnOff(){
关闭微波
断电
}
public float getTemperature(){
获取传感器的温度
}
}
public class Machine_C implements Machine{
public void turnOn(){
通电
}
public void warmUp(){
量子缠绕
}
public void turnOff(){
断电
}
public float getTemperature(){
获取传感器的温度
}
}
到这里,我们实现了 热水壶 和他的 电机 之间的解耦,当我需要创建某种产品的时候,只需要从 Kettle 的类树中挑一个具体的子类和 Machine 的子类中挑一个具体的子类组合就可以了,这完全由 client 在调用他们的时候自己决定
而这是一个标准的桥接实现
你发现了吗,Kettle和Machine的子类自始至终都没有发现对方的存在,他们之间的关联只存在于他们的根类之间
这也是桥接的特点之一,桥接把耦合集中在最根部的部分中,而给予了子类极大的自由度,大到我在运行的过程中切换了别的Machine其实Kettle也不知道
上述的例子展示了桥接对两个都会进行拓展的模块是如何解耦的
事实上桥接还有一个作用,当你想在多个对象间实现共享内容,同时又不希望外部知道这件事的时候,桥接可以帮你实现
比如 热水壶 中 Machine 类可以共享一个计数器,当有新的 Machine 被创建的时候 计数器自动+1,而Kettle不会知道这个计数器的存在
在上文的例子中我们只是讲到了桥接模型的实现,并没有涉及到 client 代码最终到底要如何创建被桥接的模块的具体对象
对于这种情况我们一般也不会去硬编码,参数工厂方法 和 生成器模式 都是可以考虑的方式,通过这些创建型模式来规定桥接对象中的组装方式,会让你的代码更加优雅