noteless 头像

桥接模式 桥梁模式 bridge 结构型 设计模式(十二)

 
桥接模式Bridge
image_5c00db0d_60a6
 
Bridge 意为桥梁,桥接模式的作用就像桥梁一样,用于把两件事物连接起来
 

意图

将抽象部分与他的实现部分进行分离,使得他们都可以独立的发展。 

意图解析

依赖倒置原则要求程序要依赖于抽象接口,不要依赖于具体实现。
简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合
抽象
抽象就是将多个事物、实体中共同的概念提取出来
比如,一组共同特性的对象概念,可以提取出来类
如果一些类又具有共同的概念性联系,又可以提取出来抽象类和接口
实现
抽象的具体,就是实现
比如一个对象是一个类的实现,一个具体的子类是抽象父类的实现 

类的功能层次结构
按照依赖倒置原则,我们面向抽象进行编程
通常会使用接口或者抽象类用于描述功能概念
然后通过继承进行功能概念的扩展,子类继承父类,并且扩展父类以增加新的功能
类的实现层次结构
在基于功能层次结构的基础上,需要对方法接口或者概念等进行具体实现
这些所有的实现就组成了类的实现层次结构

比如:
定义一个图片编辑器imageEditor(接口)
分为windows和Linux两个平台(接口)
然后又分别有两个实现类windowsImpl 以及LinuxImpl(实现类)
image_5c00db0d_2922
图中,红框部分即为类的功能层次结构,蓝色矩形框部分即为类的实现层次结构

对于类的层级结构不是很复杂的时候,我们可能经常会将类的功能层次结构和实现层次结构掺杂在一起
比如,你可能定义了一个抽象类A,然后他有两个子类B和C,B是用于实现,另一个C却是功能逻辑的扩展
当层次接口相对比较简单的时候,掺杂在一起,还相对容易应对
当层次结构变得很复杂时,如果还是掺杂在一起将会变得非常难以处理
因为当你想要扩展产品功能逻辑时,你可能很难确定到底应该在类的哪一个层次结构中去扩展

我们将编辑器抽象提取出来Editor(接口)
又有图形编辑器ImageEditor(接口)文本编辑器TextEditor(接口)视频编辑器VideoEditor(接口)
又分别有windows和linux两个版本的软件
红色框内为功能层次结构,蓝色框内为实现层次接口
image_5c00db0e_7d15
上图就是通过继承的形式进行功能扩展与实现
可以看得出来,如果新增加一种新的编辑器,比如音频AudioEditor
那么可能需要新增加AudioWindowsEditor和AudioLinuxEditor以及他们的实现类AudioWindowsEditorImpl和AudioLinuxEditorImpl
除了新增的编辑器外,新增文件个数为4
如果,当新增加一种操作系统,比如 Os X
那么,将需要根据现有的Editor类型 创建对应的三个Os X操作系统的接口(ImgXEditor,TextXEditor,VideoXEditor)
然后在创建与之对应的三个实现类(ImgXEditorImpl,TextXEditorImpl,VideoXEditorImpl)
除了新增的操作系统外,新增文件个数为6
这种采用多层继承结构的形式,类的个数巨大
因为不仅仅有多种类型的Editor,设类型个数为X
又需要在多个操作系统平台上进行实现,设平台个数为Y
实现类的个数为X*Y
扩展时,如上例,个数又将会爆发式的增长
随之而来的就是维护、使用、运行等成本的增加
 
继承从一开始就把抽象角色和实现角色进行了绑定,是一种强关联关系,并不符合组合复用原则
编译时期就已经确定,不能够在运行时期进行变动
而且,继承将父类暴露给子类,如果父类发生变化,子类势必将会受到波及影响,将不得不做出修改
不符合开闭原则

再有就是,对于每一个实现类,他即涉及具体类型的Editor又涉及平台,比如ImgWindowsEditor
用于处理图像img 又涉及到windows平台,那么涉及到img或者windows的修改,都可能会影响他导致修改
不符合单一职责原则

面对复杂继承层次结构带来的问题,所以,人们希望能够
将抽象部分与他的实现部分进行分离,使得他们都可以独立的发展。
这就是桥接模式的最初动机

向分离的演进

仔细观察可以发现,之所以类会如此膨胀,扩展如此困难的原因就在于他不止一个维度
Editor类型以及不同平台实现两个维度
也正是这两个维度导致了不符合单一职责原则
image_5c00db0e_3871

是否可以将这两个维度进行分离?
如果能够分离,也就意味着不会是完全使用继承层次结构
因为继承是强关联,完全继承,就不是分离
那么,就需要考虑类的组合
如果能够分离,将他们拆分为各自不同的维度,那么就不会出现类的个数爆炸式增长的情况
因为一旦分离,就可以各自独立发展
独立发展,那么增加一个Editor类型就是一个Editor类型
增加一个操作系统平台,就只是增加一个操作系统平台类型
不在爆炸增长
如果能够进行分离,那么他们各自负责自己不同的维度,职责将会更加单一
 

客户端关注什么?

客户端程序的目的在于使用Editor,也就是Img、Text、Video的编辑
他其实并不在意到底是什么平台
而且,他也不应该在意,只有这样才能够跨平台
但是,在我们的类层次结构中,却偏偏的与平台建立了强关联

所以一种解决方案就是:
 
客户端面向Editor进行编程
Editor不关注平台的细节,将与平台相关的实现剥离开来
而平台的实现部分,通过组合的方式,组合到Editor中来
 
 
类似适配器模式(对象适配器模式),Editor作为目标对象Target
而与平台相关联的实现就是被适配的角色Adaptee
而每一个类型比如ImgEditor都是Adapter
image_5c00db0e_6270
关于平台相关的细节部分,通过组合的方式
如下图所示Editor中含有指向Implementor的引用
涉及平台相关性的处理,借助于Implementor来完成
image_5c00db0e_b2c
 

代码示例

package bridge;
/**
* 编辑器的抽象类
* 内部包含implementor 对Editor的请求可以借助于Implementor
*/
public abstract class Editor {
    protected Implementor implementor;
    public void setImplementor(Implementor implementor) {
        this.implementor = implementor;
    } 
    void doEdit(){
        implementor.systemEditor();
    }
}
package bridge;
public class ImgEditor extends Editor {
    @Override
    void doEdit() {
        System.out.println("ImgEditor working");
        implementor.systemEditor();
    }
}
package bridge;
public class TextEditor extends Editor {
    @Override
    void doEdit() {
        System.out.println("TextEditor working");
        implementor.systemEditor();
    }
}
package bridge;
public class VideoEditor extends Editor {
    @Override
    void doEdit() {
        System.out.println("VideoEditor working");
        implementor.systemEditor();
    }
}
以上代码为上图中的左半部分
image_5c00db0e_2bfb
 
package bridge;
public interface Implementor {
void systemEditor();
}
package bridge;
public class WindowsImpl implements Implementor {
@Override
public void systemEditor() {
System.out.println("working on windows platform");
}
}
package bridge;
public class LinuxImpl implements Implementor {
 @Override
 public void systemEditor() {
  System.out.println("working on linux platform");
 }
}
以上为右半部分的实现
image_5c00db0e_af9

测试代码
package bridge;
public class Test {
public static void main(String[] args) {
Editor editor = new ImgEditor();
editor.setImplementor(new WindowsImpl());
editor.doEdit();
}
}

 

image_5c00db0e_326e
 
 
在示例代码中,创建了一个ImgEditor
然后动态的借助于new WindowsImpl()  在windows平台上执行编辑任务
在真正的借助于平台底层,执行平台相关的代码之前,还进行了一些其他的处理
比如上面的打印语句:
    System.out.println("ImgEditor working");
 
这种使用方式,用户关注的是Editor,不在与具体的平台进行绑定
具体的平台通过组合的形式组合到Editor中
在具体的使用到平台相关的方法中,Editor依赖于内部的Implementor 进行处理
扩展时,也不会出现类文件个数爆炸增长的问题
可以相互独立发展,这就是桥接模式      

结构

image_5c00db0e_14ad

抽象化角色Abstraction
抽象化给出定义,并保存一个对实现的引用
修正抽象化RefinedAbstraction
扩展抽象化角色,调整父类对抽象化的定义
实现角色Implementor
给出实现化角色的接口定义,但不给出具体的实现
这个接口不一定和Abstraction中的接口定义相同,实际上,可以完全不相同也没关系
实现化角色仅仅给出底层操作抽象化角色给出基于底层操作,更高一层的抽象操作
比如底层是基于平台的,更高一层则是ImgEditor这种
具体实现化角色ConcreteImplementor
给出实现化角色的具体实现代码

 
桥接模式的重点在于理解抽象化与实现化的概念含义
不要局限在java中定义一个接口A,然后定义一个实现类AImpl,然后override所有的方法
这种思维方式太狭隘了
抽象化在于针对于底层操作的更高一层抽象,更高一层的调用
就像上面的例子,ImgEditor真正的实现需要依赖底层具体的平台,所以ImgEditor的doEdit方法是底层平台实现的抽象化

千万不要把抽象与实现局限在extends 和implements关键字的那种形式
应该认为,但凡是更高一层的调用,或者封装,都可以认为是一种抽象与实现
也正是因为这种模式是extends 和implements关键字场景的进一步抽象
所以也被称之为接口Interface模式

extends 和implements关键字的形式自然是抽象与实现
一个对象中的方法,借助于另外的对象来实现,这也是一定程度上的“抽象化--实现化”
所以说适配器模式也是一定程度上的抽象化--实现化”

抽象化对象就像是手柄一样,通过手柄来操纵委派给实现化角色,所以桥梁模式也被称之为柄体模式 Handle and Body

抽象化等级结构中的方法,通过向对应的实现化对象委派实现自己的功能
这就意味着抽象化角色可以通过向不同的实现化对象委派就可以达到动态的转换自己的功能的目的

在上面的示例中,我们使用
public void setImplementor(Implementor implementor)
进行Implementor的设置
一般经常使用工厂模式(方法)进行创建赋值
 

桥梁模式与JDBC

jdbc的百度百科
image_5c00db0e_3248
JDBC是桥梁模式的典型应用
他将抽象化与实现化进行分离
image_5c00db0e_61e3
它为所有的关系数据库提供了一个通用的访问界面
提供了一组与具体厂家实现完全无关的接口
有了JDBC这一组通用接口
应用系统就可以不依赖数据库引擎的具体细节而独立的演化

一个应用系统动态的选择一个合适的驱动器,然后通过驱动器向数据库引擎发出指令
这就是抽象角色的行为委派给实现角色来完成任务

厂家的实现与JDBC之间,并没有任何的静态强关联

其实很多其他形式的驱动又何尝不是如此?
比如Office办公软件打印不需要关注于具体的打印机厂家型号
会有统一的驱动为我们进行处理
 

模式对比

与适配器区别

image_5c00db0e_797d

上面说到桥接模式类似适配器模式
而且,某种程度上讲,适配器模式也符合“抽象化---实现化”的概念
而且,从上面的结构图中,可以看得出来,桥接模式与对象适配器模式的相似程度
他们都拥有抽象的概念角色 Abstraction和Target
它们都拥有具体的客户端需要直接面对的角色 RefinedAbstraction和Adapter
他们都有工作需要委托给内部的“工作人员”Implementor 和 Adaptee
他们都依赖于“抽象”与“实现”的概念

那么到底有什么区别呢?
适配器模式的主要目的是让因为接口不兼容而不能互相工作的类能够一起工作
换句话说就是他们本身不同,我用“纽带” Adapter将他们连接起来
而桥接模式则是将原本或许紧密结合在一起的抽象与实现,进行分离
使她们能够各自独立的发展,是把连接在一起的两个事物,拆分开来
然后用“纽带”“桥梁”(也就是对象的引用)将他们连接起来

适配器模式就好比张三和王五不认识,李四介绍他们认识
桥梁模式好比张三和王五成天黏在一起活干得不好太乱套,李四说以后我作为接口人,你俩各干各的吧
虽然看起来都是两个人,中间一个联系人,但是含义却是完全不同
 

与装饰器区别

装饰器模式中,使用组合而不是继承来对类的功能进行扩展,避免了类的个数的爆炸增长,与桥梁模式的结果不约而同
都解决了类爆炸增长的问题,都避免了过多的没必要的子类

装饰器模式侧重于功能的动态增加,将额外的功能提取到子类中
通过不同的排列组合,形成一个递归的调用方式,以动态的增加各部分的功能

桥梁模式是将原本系统中的实现细节抽取出来,比如原来抽象概念与实例化全部都是一个类层次结构中
把所有的实现细节,比如示例中的平台相关实现,抽取出来,进行分离
达到抽象与实现分离的目的

所以虽然他们都可以解决子类爆炸式增长、不易扩展的问题
但是他们的出发点完全不同
一个关注于功能的动态扩展组合
一个关注于抽象与实现的分离,获得更多的灵活性

总结

场景

如果一个系统要求抽象化角色和具体化角色之间增加更多的灵活性
避免在两个层次之间建立静态的联系
也就是实现化角色的改变,不会影响客户端,完全透明的

如果系统需要在多个抽象化角色和实现化角色之间进行动态的耦合
也就是要求能够动态的组合

如果使用多层继承结构进行处理时,就可以考虑使用桥接模式
尤其是一个类存在两个或者多个变化的维度,而且这两个维度也可能需要动态的扩展

总之,当你需要抽象化和是实现化进行解耦时,你就可以考虑桥梁模式
解耦就会变得透明不会互相影响能够独立发展,解耦就能够动态的组合,解耦了就不会有静态的联系
 

优点

提高了扩展性,多层继承的良好替代方案
将抽象与实现进行解耦,更加符合单一职责原则 组合复用原则以及开闭原则
 

注意点

想要使用桥接模式,必然要理清楚你面对的需求中抽象与实现的部分,才能更好的进行运用。
只要具备抽象与实现分离的相关需要,都可以考虑桥梁模式

前面描述的多个维度不同发展,多层次的抽象,是桥梁模式的更高级别的运用,必须要先分析清楚变化的维度
而且最重要的就是分析出整个类层次结构中的“抽象化”部分和“实现化”部分
我们的Editor示例中,ImgEditor TextEditor才是客户端程序关注的,他们不希望关注于具体平台
JDBC中,客户端程序关注的是数据库的查询操作行为,而不希望关注数据库的细节
所以你必须找准到底谁是抽象化

桥接模式的场景理解起来略微有点费神
但是他的根本逻辑却是非常简单,那就是抽象的概念功能与具体的实现进行分离
桥接模式是面向抽象编程--依赖倒置原则的具体体现
并且使用组合的形式,解决了多层继承结构中的一些问题
posted @ 2018-11-30 14:56  noteless  阅读(6938)  评论(0编辑  收藏  举报