设计模式预计阅读 14 分钟5226 字1907 次浏览

设计模式修炼之路 ⑦ 桥接模式——m×n 降为 m+n

继承爆炸的解药、m×n 降为 m+n、在 JDBC 里找它

目录

上一篇适配器模式治好了"接口对不上",结尾留了个尾巴:当两个维度一起膨胀——形状要乘以颜色、品牌要乘以型号——继承图开始爆炸时,该请谁出场?答案就是结构型模式的第二位选手:桥接模式(Bridge Pattern)。适配器是事后买转接头,桥接是事前就想明白:"这事儿明明有两个会变的维度,为什么非用继承焊死在一起?"

一、什么是桥接模式?

先看定义:

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

翻译成人话:当"做什么"和"怎么做"是两个独立变化的维度时,别用继承硬扛,把一个维度组合进另一个维度,让两边各自演化。

生活类比就用画图这件事本身:画图天生有两个维度——画什么(圆、方、三角……)和用什么颜色画(红、绿、蓝……)。没有人会为"红圆、绿方、蓝三角"每种组合专门备一支画笔——圆规、三角板是工具维度,红颜料、绿颜料是颜料维度,分开备货,用时蘸一下就组合。桥接模式干的就是这件事:把两个维度拆成两棵独立的继承树,再用组合把它们连起来。

桥接模式有四个角色:

  • Abstraction(抽象化):形状维度的顶层。它知道"画什么",但不亲自动手,把"怎么上色"委托给手里的实现者;
  • RefinedAbstraction(扩充抽象化):Abstraction 的子类,Circle、Square——形状维度上的具体成员;
  • Implementor(实现者接口):颜色维度的接口,DrawAPI。注意它不实现 Abstraction,它是另一棵独立的树;
  • ConcreteImplementor(具体实现者):RedDraw、GreenDraw——颜色维度上的具体成员。

最容易犯的迷糊在这里:定义里的"抽象"和"实现",不是"abstract 父类和它的子类"那种关系,而是两棵平行的树:一棵管"做什么"(形状),一棵管"怎么做"(上色)。Abstraction 持有 Implementor 的引用,这条跨树的组合关系,就是那座"桥"——名字由此而来。

二、桥接模式的优缺点

优点

  1. 两个维度独立扩展,互不牵连。加三角形只动形状树,加紫色只动颜色树,谁也不碰谁——完美符合开闭原则。
  2. 类数量从 m×n 降到 m+n。5 种形状 × 8 种颜色,继承写法要 40 个组合类,桥接只要 13 个(5 + 8)。维度越多、成员越多,这笔账越吓人。
  3. 运行时可切换实现。构造函数传哪支"颜料",图形就长什么色,同一棵形状树配任何颜色树都行。
  4. 细节对客户端透明。客户端只面对两棵树的抽象层做组合,不用关心具体类怎么配对。

缺点

  1. 得先看穿"有两个独立维度"。桥接是预防针,得在设计期就识别出变化维度;要是等继承图炸了才重构,成本不小。
  2. 增加理解成本。"Shape 里怎么攥着个 DrawAPI"——一条跨树引用不如一棵继承树直观,新人第一眼容易懵。
  3. 维度识别错了全白搭。如果两个维度其实强耦合(比如颜色离开具体形状毫无意义),硬拆反而把简单问题搞复杂。

三、桥接模式的应用场景

桥接模式在 JDK 和主流框架里都有教科书级的案例:

  • JDBC:最经典的一例。你调 DriverManager.getConnection() 拿到的是 java.sql.Connection 接口(抽象),底下真干活的,是 MySQL / Oracle / PostgreSQL 各家驱动包里的实现类(实现)。换数据库,改改 JDBC URL 和驱动依赖,业务代码一行不动——"SQL API"和"具体驱动"两个维度各自变化、各自升级,互不拖累。
  • AWT:java.awt.Component(按钮、文本框,抽象树)和 ComponentPeer(Windows / X Window / Mac 的本地实现,实现树)。按钮不关心自己跑在哪个操作系统上,渲染的事全权委托给 Peer——Java GUI "一次编写到处运行",桥接立了大功。
  • 日常业务里:支付场景——支付方式(微信/支付宝/银联)× 支付渠道(APP/H5/小程序),用继承写就是 WxAppPay、WxH5Pay、AliAppPay……8 个类起步,每加一个维度成员爆炸一轮;桥接拆成"方式树 + 渠道树"立刻清爽。消息推送同理:消息类型 × 发送通道(短信/邮件/站内信/钉钉)。

什么时候该用:需求里一眼能看出两个天生独立、且都会持续增加成员的维度(形状×颜色、方式×渠道、品牌×型号),组合又自由多变——提前用桥接拆树,而不是等继承图爆炸了再补。

四、桥接模式的实现方式

示例代码在 design-pattern 项目的 bridge 包里:

design-pattern/src/main/java
└── bridge                       // 桥接模式
    ├── DrawAPI.java             // 实现者接口:颜色维度
    ├── RedDraw.java             // 具体实现者:红
    ├── GreenDraw.java           // 具体实现者:绿
    ├── BlueDraw.java            // 具体实现者:蓝(演示"新增只加一个类")
    ├── Shape.java               // 抽象化:形状维度,持有桥
    ├── Circle.java              // 扩充抽象化:圆
    ├── Square.java              // 扩充抽象化:方形
    ├── BridgeTest.java          // 测试入口
    └── explosion                // 反面教材:继承爆炸
        ├── RedCircle.java
        ├── GreenCircle.java
        ├── RedSquare.java
        └── GreenSquare.java

场景:画图软件要支持多种形状 × 多种颜色。先看用继承硬扛的下场,再看桥接怎么救。

1. 反面教材:继承爆炸

最直觉的写法:形状当父类,颜色当子类分支——每种"形状×颜色"组合一个类:

public class RedCircle {

    public void draw() {
        System.out.println("用 RedCircle 类画了一个红色的圆形");
    }
}

GreenCircle、RedSquare、GreenSquare 全部照抄,只是类名和打印文案不同。2×2=4 个类,目前还忍得了,跑起来是这样:

===== 反面教材:继承爆炸 =====
用 RedCircle 类画了一个红色的圆形
用 GreenCircle 类画了一个绿色的圆形
用 RedSquare 类画了一个红色的方形
用 GreenSquare 类画了一个绿色的方形

但数学账不骗人:加一个三角形,4 变 6;再加一个蓝色,6 变 9;真实系统里 10 种形状 × 8 种颜色 = 80 个类,每个类都在复制粘贴大同小异的代码。更要命的是,新增任何一个维度的成员,都要横跨整个继承图补一排类——"一处新增,处处开工",开闭原则碎了一地。

2. 桥接版:两棵树 + 一条组合线

换个思路:形状和颜色明明是两个维度,凭什么让颜色"寄生"在形状的继承树里?把它请出来,单独站成一棵树。

先立实现者接口——颜色维度,只管"怎么画":

// 实现者接口:颜色维度,只管"怎么画"
public interface DrawAPI {

    void draw(String shape);
}

再来两个具体实现者:

// 具体实现者:红色画笔
public class RedDraw implements DrawAPI {

    @Override
    public void draw(String shape) {
        System.out.println("用红色画笔" + shape);
    }
}

GreenDraw 照抄换个颜色字。BlueDraw 也一样——注意,它是后来"新增蓝色"时只需要新增的一个类,形状树一行没动:

// 具体实现者:蓝色画笔,新增颜色只加这一个类
public class BlueDraw implements DrawAPI {

    @Override
    public void draw(String shape) {
        System.out.println("用蓝色画笔" + shape);
    }
}

然后是主角,抽象化——形状维度,只管"画什么"。重点就一行:持有实现者的引用,这条组合关系就是桥:

// 抽象化:形状维度,只管"画什么"
public abstract class Shape {

    protected final DrawAPI drawAPI; // 抽象化持有实现者的引用,这条组合关系就是桥

    protected Shape(DrawAPI drawAPI) {
        this.drawAPI = drawAPI;
    }

    public abstract void draw();
}

扩充抽象化继承它,画的时候把"上色"委托给手里的画笔:

// 扩充抽象化:圆形
public class Circle extends Shape {

    private final double radius;

    public Circle(DrawAPI drawAPI, double radius) {
        super(drawAPI);
        this.radius = radius;
    }

    @Override
    public void draw() {
        drawAPI.draw("画了一个半径" + radius + "的圆");
    }
}

Square 只是把"半径"换成"边长",其余雷同。最后看测试代码——两个维度像两排积木,自由拼装:

private static void bridge() {
    DrawAPI red = new RedDraw();
    DrawAPI green = new GreenDraw();
    DrawAPI blue = new BlueDraw();
    Shape redCircle = new Circle(red, 3);
    Shape greenCircle = new Circle(green, 3);
    Shape blueCircle = new Circle(blue, 3);
    Shape redSquare = new Square(red, 2);
    Shape greenSquare = new Square(green, 2);
    Shape blueSquare = new Square(blue, 2);
    redCircle.draw();
    greenCircle.draw();
    blueCircle.draw();
    redSquare.draw();
    greenSquare.draw();
    blueSquare.draw();
}

运行结果:

===== 桥接版:两个维度独立变化 =====
用红色画笔画了一个半径3.0的圆
用绿色画笔画了一个半径3.0的圆
用蓝色画笔画了一个半径3.0的圆
用红色画笔画了一个边长2.0的方形
用绿色画笔画了一个边长2.0的方形
用蓝色画笔画了一个边长2.0的方形

同样覆盖全部组合,账本完全不同:

  • 反面教材覆盖 4 种组合用了 4 个类;桥接版覆盖 6 种组合,两棵树加起来 2 + 3 = 5 个类;
  • 反面教材每加一种颜色,全树横着补一排类;桥接版加蓝色,只加一个 BlueDraw,Shape、Circle、Square 一个字没改;
  • 反过来加三角形也一样——新建 Triangle extends Shape 完事,颜色树原地不动。

这正是组合优于继承的第二次大显身手。适配器篇说过它会反复出场:对象适配器用组合替换继承换来灵活,这次桥接用组合替换"多维继承"换来的是整个维度树的自由。继承表达的是"是一种"(圆是一种形状),颜色和形状之间可没有"是一种"关系,只有"搭配使用"关系——用继承表达搭配,从一开始就用错了工具。

五、适配器 vs 桥接:同样是"中间加一层"

桥接和适配器经常被新人搞混,因为乍看都是"两个东西之间加一层"。但仔细对照,差别全是本质性的:

适配器模式 桥接模式
解决什么问题 接口不兼容 多维度组合膨胀
出场时机 事后补救:系统成型后发现对不上 事前设计:动工前就识别出变化维度
中间层的身份 转换器:把 A 的接口翻译成 B 连接线:两棵树之间搭组合关系
有几棵树 一棵(Adaptee 被包进 Adapter) 两棵(形状树 + 颜色树平行独立)
扩展方向 对齐一个既有接口 两个维度各自加成员
类比 买转接头 提前分柜备货

一句话总结:适配器改变接口的"样子",桥接不改变任何接口,只是把"变化"本身分类安置。它俩甚至能配合出场——先用桥接把维度拆开,某天某个具体实现者和新体系接口对不上,再拿适配器救个急。

顺带澄清一个名字误会:上一篇提到的 log4j-over-slf4j 常被叫作"桥接包",但看它的结构——把老框架 API 翻译到新门面,干的是适配器的活儿,只是社区叫顺了口。判断模式看结构意图,别看名字。

写在最后

回顾这一篇:桥接模式把"两个一起膨胀的维度"从一棵继承树拆成两棵独立的树,用一条组合关系当桥,类数量从 m×n 降到 m+n;四个角色里,Abstraction 持有 Implementor 引用的那行代码就是全部精髓;JDBC 的 Connection 与各家驱动、AWT 的 Component 与 Peer,是它在 JDK 里的高光时刻;它和适配器的区别一句话——适配器是事后急救的转接头,桥接是事前预防的分柜设计。

下一篇继续结构型模式,第三位选手,也是一位编外人员:过滤器模式(Filter Pattern)。它不在 GoF 23 人名单里,却是业务代码里的常客:把一堆对象按条件筛来筛去,条件还能像积木一样自由拼接——与、或、非随便组合。列表页的多条件筛选、权限系统的规则判断,背后都是它的影子。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论