桥接模式
1.什么是桥接模式
将抽象部分与它的实现部分解耦
,使他们都可以独立地变化
。这种类型的设计模式属于结构型模式
,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
桥接模式是一种很实用的结构型设计模式,在软件开发时,如果某个类存在两个独立变化的维度,可以运用桥接模式将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。
与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系就像一条桥一样,将两个独立继承结构的类联接起来,故名桥接模式。
可以明显看出,桥接模式使用组合代替了继承,将类之间的静态继承关系转换为动态的对象组合关系,使用组合而不用继承
,会使系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。
2.示列图
image.png可以看到在桥接模式的结构图中,存在一条连接两个继承等级结构的桥。
在桥接模式结构图中包含如下几个角色:
-
Abstraction(抽象类)
:用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。 -
RefinedAbstraction(扩充抽象类)
:扩充由Abstraction定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用在Implementor中定义的业务方法。 -
Implementor(实现类接口)
:定义实现类的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction中不仅拥有自己的方法,还可以调用到Implementor中定义的方法,使用关联关系来替代继承关系。 -
ConcreteImplementor(具体实现类)
:具体实现Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同实现,在程序运行时,ConcreteImplementor对象将替换其父类对象,提供给抽象类具体的业务操作方法。
桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。
在使用桥接模式时,首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。
3.典型代码
一个类两个独立变化的维度,为了降低耦合度,对两个不同的维度提取抽象类和实现类接口,并建立一个抽象关联关系。对于实现部分维度,实现类接口典型代码如下:
public interface Implementor{
void operationImpl();
}
具体实现类中实现了在实现类接口中声明的方法,典型代码如下:
public class ConcreteImplementor implements Implementor{
public void operationImpl(){
//todo
}
}
另一维度,抽象类典型代码如下:
public abstract class Abstraction{
protected Implementor impl; //实现类接口
public void setImpl(Implementor impl){
this.impl=impl;
}
public abstract void operation();//声明抽象业务方法
}
扩充抽象类继承抽象类,典型代码如下:
public class RefinedAbstaction extends Abstraction{
public void operation(){
//todo
impl.operationImpl();
//todo
}
}
看上去有点像 多维度的装饰着模式
。
4.代码示例
假如有三个品牌的手机vivo,oppo和小米,每个手机需要手机壳保护,手机壳有简单型手机壳和可爱型手机壳。这种处理多维变化(手机和手机壳)的方式运用到软件设计中就是桥接模式。
先定出抽象部分:
public abstract class Phone {
protected ShellImplementor shellImplementor;
public void setShellImplementor(ShellImplementor shellImplementor){
this.shellImplementor = shellImplementor;
}
public void call(){
System.out.println("打电话");
}
public abstract void playMusic();
}
扩展抽象部分:
public class VivoPhone extends Phone {
public void playMusic() {
shellImplementor.wearShell();
System.out.println("音乐High起来");
}
}
public class OppoPhone extends Phone {
public void playMusic() {
shellImplementor.wearShell();
System.out.println("音乐high起来");
}
}
public class XiaomiPhone extends Phone {
public void playMusic() {
shellImplementor.wearShell();
System.out.println("音乐high起来");
}
}
将手机壳部分定义为实现部分:
public interface ShellImplementor {
void wearShell();
}
具体实现:
public class SimpleShell implements ShellImplementor {
public void wearShell() {
System.out.println("戴上简单手机壳");
}
}
public class CuteShell implements ShellImplementor {
public void wearShell() {
System.out.println("戴上可爱手机壳");
}
}
客户端测试:
public class Client {
public static void main(String[] args) {
Phone phone = new VivoPhone();
ShellImplementor shell = new SimpleShell();
phone.setShellImplementor(shell);
phone.playMusic();
}
}
新增手机型号只需继承自Phone就可以,新增手机壳也只需实现ShellImplementor,而且更换简单,可以用XML配置文件实现,只需修改配置,不需修改源码。
而且如果还要多一个实现,即三重维度,比如手机膜,多重继承会直接炸掉,而桥接模式则相对简洁很多。
5.使用场景
在以下情况下可以考虑使用桥接模式:
- 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
6.Android中的桥梁模式应用:
在编程实践中,严格按照桥梁模式来开发程序时不常见,前面说过,我们平时经常面对的是具体业务场景,自然而然的使用具体的类来实现功能。让一个具体的类来委托另一个具体的类来实现功能,是最为常见使用场景,很少写出桥梁模式的代码来。不过在一些应用框架中,经常会使用一些标准的桥梁模式,目的是使模块之间能够灵活扩展和解耦,比如Android中就有一些典型的桥梁模式的应用。
下面分别分析一下:
1、AdapterView与Adapter:
对照桥梁模式的定义和结构图,我们发现AdapterView类是抽象角色,Adapter是实现角色,AdapterView所呈现的UI界面不是自己实现的,即它的子View是由Adapter决定的,也就是说AdapterView的职责是实现一组View的展现,但实现的具体方法要委托Adapter来提供。
我们知道,AdapterView有子类ListView、GridView、Spinner。。。等,使用时要给它set一个Adapter类型的对象,这个Adapter也是抽象类,也有一些子类,如ArrayAdapter、SimpleAdapter。。。等。在实际使用时,我们会创建一个具体的XXXAdapter对象,让ListView、GridView等使用,它们之间是独立变化的,没有互相影响,确实是桥梁模式应用。
2、Window与View:
Android中所有的视图View都是通过Window来负责展现的,无论是Activity、Toast、Dialog还是PopupWindow,最终都是要attach到Window上的,Window是View的直接管理者。从Window类的注释可以看出,它是一个顶级窗口外观和行为的抽象基类,提供了标准的UI策略,如背景、标题栏、缺省按键处理等,而响应用户显示操作的由内容View负责处理。
Window对象与View之间是如何协作的呢?就以Activity来说吧,Activity要显示UI界面时,首先要获得一个Window对象,并把各个View控件添加到Window中去。在实际开发时,Activity给它什么View,它就显示什么View。Window把如何显示内容UI(即content view)全部委托给了View类,而View又有一系列的Layout、View、Widget等众多子类,Window类也有子类,它们都可以独立的变化,典型的桥梁模式应用,即Window是抽象角色,而View是实现角色。我们在开发时,可以在Activity中随意创建布局,并把布局作为参数调用setContentView(),只修改Activity就行了,对Window没有半毛影响。
按照桥梁模式的原理,按说Activity也能随意创建一个Window对象,对ContentView里面的View也没半毛关系。不过在当前android的SDK中,Window只有一个子类,即PhoneWindow,这样就成了抽象角色只有一个具体类,Activity也没得选择,不能独立变化,只能使用这个唯一的子类。我们先看一下Window里面的说明:The only existing implementation of this abstract class is android.policy.PhoneWindow, which you should instantiate when needing a Window. Eventually that class will be refactored and a factory method added for creating Window instances without knowing about a particular implementation.大意是Window目前只实现了一个子类PhoneWindow,将来会重构这个类,并使用工厂方法来创建具体的Window对象,至于创建什么样的对象,调用者就不用关注了,工厂会按照一定的场景创建合适的Window对象。这就不得不让我们有这样的思考:比如,如果将来Android手机、AndroidPad和AndroidTV的UI整体界面发生了很大的变化,那么就可以定义一个Window的子类来适应新场景了,比如定义子类PadWindow类用于Pad设备,定义子类TvWindow类用于电视设备,而PhoneWindow只用于手机。工厂方法根据所用设备的类型创建不同的Window对象实例就行了,对市面上已经存在的App应用没有任何影响,同样的View在不同的Window对象上会有不同的展现,也就是说通过该模式已经为将来可能产生的变化做了封装,这也正是Android框架的优秀之处,体现了它的前瞻性。当然这纯属个人猜测,Android框架以后是否会按照上面分析的来演进,拭目以待吧。