桥接模式

144 阅读6分钟

桥接模式是一种结构型设计模式,它用来将一个大类,或一系列紧密相关的类,拆分为抽象实现两个独立的层次的互不影响的结构。但是我们需要明确一点,它在我们日常开发中并不常用,但是对我们理解面向对象设计有很大的帮助。

桥接模式的概念

桥接模式会将代码中的抽象部分,与它的实现部分分离,使它们都可以独立地变化。

独立变化的意思就是分离后的各个结构部分可以自由地增加一些对应的实现,比如说将咖啡抽象成杯量大小和口味不同两个维度,针对每个维度都可以自由地新增不同的实现,杯量可以新增中杯、大杯、超大杯、迷你杯、节日杯......。口味可以新增加糖、无糖、加奶、加糖且加奶......。

通过不同层次的概念的独立变化来完成相应的业务需求开发。

桥接模式又被称为接口(Interfce)模式

首先我们来分析这个模式的名字。

桥在我们日常中的作用就是将“两岸”连接起来。

将这个概念应用到开发中,它的作用就是将两个独立的结构连接起来,而这两个结构本身是可以独立变化的,即它们各自可以根据要求生成不同的实例。

使用场景

这里挑选比较易于理解的官方的解释加上自己的理解来做分析:

  1. 一个类存在两个(或多个)独立变化的维度(如上面举例的咖啡),且这两个(或多个)维度都需要独立进行扩展(新增某个维度下的不同的拓展)。

  2. 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

桥接模式的组成结构

首先我们来看桥接模式的类图:

image.png

桥梁模式的组成角色包括:

  • 抽象化(Abstraction)角色:对给出的定义进行抽象化,并保存一个对实现化对象的引用。可能这句话看起来就比较抽象不好理解,我们暂时先知道这个概念,对比之后的代码就可以比较好理解。
  • 修正抽象化(RefinedAbstraction)角色:对抽象化角色进行扩展,对父类对抽象化的定义进行扩展或者修正。
  • 实现化(Implementor)角色:这个角色是实现化角色的接口,但没有具体的实现。我们需要注意,这个接口不需要和抽象化角色的接口有相同的定义,这两个接口可以非常不一样。在实现化角色中,只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
  • 具体实现化(ConcreteImplementor)角色:这个角色实现了实现化角色接口,给出具体操作逻辑。

使用桥接模式的意义

当我们想要拆分或者重组一个具有多重功能的,比较庞大的类,这时候就可以考虑使用桥接模式。

在开发中,类的代码越多,对这个代码块进行修改所需要花费的时间就会越长。同时,我们在阅读相关代码来了解业务和它的运作方式就会越困难。

同时,如果一个类比较庞大,那么一个功能上的变化可能需要在整个类范围内进行修改,这样就常常会产生一些预想不到的错误,甚至还会有很多严重的副作用。

这个时候,桥接模式就可以登场了。桥接模式可以将庞杂的大类拆分为几个层次的结构。在开发中,我们可以修改任意一个层次结构却不会影响到其他层次的结构。通过这种方法可以简化代码的维护工作,在处理新业务的时候,如果要修改已有代码,也会把风险降到最低。

使用举例

这里我们使用做咖啡的场景来举例。每一杯咖啡咖啡都有杯量大小,比如“中杯、大杯、超大杯、迷你杯、节日杯”等等。同时每一杯咖啡都有对应的口味,比如“拿铁、卡布奇诺、美式”等等,但是口味我们可以简单地对其抽象为:“加奶,加糖,加奶且加糖,原味”。

根据这个场景,我们结合桥接模式来分析。

如果不使用桥接模式,杯量(5种)和口味(4种)组合在一起,就有20 种不同的产品,如果直接开发,可能就要20 个类,如:MiddleNoSugarCoffeeLargeMilkCoffee等。这个时候,如果再推出一个“加奥利奥”口味,那么就需要再写5 个类。这样显然是我们不想看到的。

那么我们现在使用桥接模式,在上一篇文章中我们知道桥接模式有四个角色:

  1. 抽象化(Abstraction)角色
  2. 修正抽象化(RefinedAbstraction)角色
  3. 实现化(Implementor)角色
  4. 具体实现化(ConcreteImplementor)角色

分析了当前业务场景,我们可以将咖啡的杯量作为抽象化Abstraction 角色,而咖啡口味为实现化Implementor 角色

第一步:创建抽象化部分:

public abstract class Coffee {
    private CoffeeAdditive additive;
    public Coffee(CoffeeAdditive additive){
        this.additive=additive;
    }
    public abstract void orderCoffee(String name, int count);
}

(additive: 添加剂)

在上述代码中,Coffee抽象类持有了CoffeeAdditive 引用,而CoffeeAdditive 的实例是通过构造函数注入的,这个过程就是桥接过程。通过这个引用就可以调用CoffeeAdditive的方法,进而将Coffee的行为与CoffeeAdditive 的行为通过orderCoffee 方法进行组合。这里看其来其实也没有什么深奥的。

第二步:创建修正抽象化角色

接下来是抽象化修正角色,里面可以新增一些需要额外附加的逻辑。比如这里我们加上去“店名”。

public abstract class RefinedCoffee extends Coffee {
    public RefinedCoffee(CoffeeAdditive additive) {
        super(additive);
    }
    public void getCafeName() {
        System.out.println("掘金咖啡北京国贸店为你服务");
    }
}

针对修正化抽象角色,我们也需要具体的实现(上面说明了,要使用杯量的维度作为抽象化角色):

public class LargeCupCoffee extends RefinedCoffee {

  public LargeCoffee(CoffeeAdditive additive) {
    super(additive);
  }

  @Override
  public void orderCoffee(String name, int count) {
    additives.addAdditive();
    System.out.println(name + " 点了" + count + " 杯大杯咖啡");
  }
}

有了抽象化相关的内容之后,接下来就是实现化的部分了。

第三步:创建实现化角色

首先是实现化角色:

public interface CoffeeAdditive {
    void addAdditive();
}

第四步:创建具体实现化角色

//加糖
public class SugarAdditive implements CoffeeAdditive {
    @Override
    public void addAdditive() {
        System.out.println("加一些糖");
    }
}
//加奶
public class MilkAdditive implements CoffeeAdditive {
    @Override
    public void addAdditive() {
        System.out.println("加100ml 奶");
    }
}

第五步:客户端调用

public static void main(String[] args) {
    RefinedCoffee largeCupWithSugarAdditive=new LargeCupCoffee(new SugarAdditive());
    largeCupWithSugarAdditive.orderCoffee("xiaoming", 1);
    largeCupWithSugarAdditive.getCafename();
}

输出结果:

加一些糖
xiaoming 点了1 杯大杯咖啡
掘金咖啡北京国贸店为你服务

总结

通过以上场景举例我们可以看出,桥接模式的使用也是比较直观的。我们主要需要做的就是理解它的思想。

以上桥接模式分析结束。