设计模式与生活⑬:你坐几号线,谁来运营是另一回事

0 阅读9分钟

一到周五,小王的心情就会好一点。倒不是工作少了,而是想到这一天忙完,后面接着双休,心里就有盼头。

地铁上,他抬头看标识,发现一件平时没留意的事:5 号线、10 号线常见的是“北京地铁”,4 号线却写着“京港地铁”。

再想想,北京的机场也各有各的名字:首都机场、大兴机场。可在成都坐地铁,日常看到的统一称谓就是“成都地铁”。

都是出行,名称和背后的主体并不完全一样。对乘客来说,常做的事还是那几样:查线路、进站、上车、到站下车。坐几号线是一条信息,谁来运营是另一条信息,不必揉成一团。

scene-subway-operators.jpg

同样是在北京坐地铁,线路和运营方并不是同一个概念

今天聊的桥接模式(Bridge),处理的就是这类问题:一个概念有两条独立变化的维度,别用继承把所有组合写死,而是用组合把它们接起来。系列第十三篇。

01 先拆清楚:是“线路”在变,还是“运营方”在变

先把场景压成最简单的模型:普通地铁线路由运营方调度,乘客调用 ride(),就能知道这条线怎么运行。

如果把线路类型和运营实现直接绑在一起,可能是这样:

class MtrRegularLine {
    void ride() {
        System.out.println("京港地铁调度,各站停靠");
    }
}

class BeijingSubwayRegularLine {
    void ride() {
        System.out.println("北京地铁调度,各站停靠");
    }
}

线路类型和运营实现,一起绑进了具体类

只有两个类,当然能跑,也没什么难懂。

可如果抽象侧还要增加“大站快车”,实现侧又要支持另一种运营系统,继承组合就会开始膨胀:MtrRegularLine、MtrExpressLine、BeijingSubwayRegularLine、BeijingSubwayExpressLine……每添一边,另一边都可能跟着复制一批。

这里的“运营调度”只是为了演示结构做的简化,不是现实地铁系统的真实接口。真正要看的是:线路类型和运营实现,是两条可以各自扩展的维度。

02 先把运营实现抽出去

给运营方留一个独立接口:

interface MetroOperator {
    void dispatch(String lineName);
}

class BeijingSubwayOperator implements MetroOperator {
    public void dispatch(String lineName) {
        System.out.println("北京地铁调度 " + lineName);
    }
}

class BeijingMtrOperator implements MetroOperator {
    public void dispatch(String lineName) {
        System.out.println("京港地铁调度 " + lineName);
    }
}

运营方只管怎么调度,不必知道自己被装进哪条线路

这一层叫实现(Implementation)。它不是单指“接口的实现类”,而是桥接模式里相对独立的一整套实现体系。

北京地铁和京港地铁只是这里的两个示例。以后再换一种运营实现,新增一个 MetroOperator 实现类即可,不需要回头给普通线路、大站快车各复制一份。

03 线路拿着运营方,这座桥就搭起来了

另一边,线路只保留一个 MetroOperator 引用。至于当前是北京地铁还是京港地铁,由组装时传进来的对象决定。

abstract class SubwayLine {
    protected final String name;
    protected final MetroOperator operator;

    SubwayLine(String name, MetroOperator operator) {
        this.name = name;
        this.operator = operator;
    }

    abstract void ride();
}

class RegularLine extends SubwayLine {
    RegularLine(String name, MetroOperator operator) {
        super(name, operator);
    }

    void ride() {
        operator.dispatch(name);
        System.out.println("普通线路:各站停靠");
    }
}

class ExpressLine extends SubwayLine {
    ExpressLine(String name, MetroOperator operator) {
        super(name, operator);
    }

    void ride() {
        operator.dispatch(name);
        System.out.println("大站快车:只停部分站点");
    }
}

线路负责自己的乘车语义,具体调度交给运营实现

SubwayLine 这一侧叫抽象(Abstraction),RegularLine 和 ExpressLine 是扩充后的抽象。它们通过 operator 这个对象引用,把该由运营方完成的工作委托出去。

这座“桥”不是一段网络连接,也不是一个名叫 Bridge 的类,就是抽象一侧持有的实现对象引用,以及随之而来的委托关系。

bridge.png

两边各自扩展,中间用组合和委托连起来

组装以后,这三条线路可以这样表示:

MetroOperator beijingSubway = new BeijingSubwayOperator();
MetroOperator beijingMtr = new BeijingMtrOperator();

SubwayLine line4 = new RegularLine("4 号线", beijingMtr);
SubwayLine line5 = new RegularLine("5 号线", beijingSubway);
SubwayLine line10 = new RegularLine("10 号线", beijingSubway);

line4.ride();
line5.ride();
line10.ride();

线路知道“我是谁”,运营方知道“怎么调度”

运行结果是:

京港地铁调度 4 号线
普通线路:各站停靠
北京地铁调度 5 号线
普通线路:各站停靠
北京地铁调度 10 号线
普通线路:各站停靠

三条线路分别委托自己的运营方,再执行各自的线路行为

新增 ExpressLine 时,两个已有运营方都能直接使用;新增运营实现时,也不必修改 RegularLine 和 ExpressLine。注意,是新增扩展时不必复制整套组合,不是任何改动都永远不用碰已有代码。接口本身一旦变化,两侧该调整还是得调整。

04 名字不同,不代表一定要架桥

这里有两处类比失真,还有一个使用边界,得挑明。

第一,现实中的线路与运营方,不可能像换接口实现那样随意插拔。车辆、信号、调度、人员和管理规范都深度关联,更换运营主体更不是改一行组装代码。地铁类比只负责说明“两条维度通过引用协作”,不负责描述真实运营交接。

第二,标识不同,不等于内部实现一定不同;标识统一,也不等于背后只有一套系统。“成都地铁”是统一呈现在乘客面前的品牌称谓,不能据此推断它的代码、设备或内部协作都只有一种。首都机场和大兴机场的名字,也不能直接拿来证明谁是桥接模式——机场名称和地铁运营标识本来就不是同一个层面的信息。

第三,如果程序里只是记录线路名和运营方名称,两个字段就够了:

record LineInfo(String name, String operatorName) {}

只保存名称时,这样最清楚,不需要桥接

桥接解决的是两条行为体系需要独立扩展,不是给所有“两个名词”都配两套继承层次。名字不同是观察的起点,不是使用模式的证据。

05 跟策略、适配器,分别差在哪儿

策略模式(Day 4)管“选一个做法完成目标”:早餐可以自己做,也可以楼下买。桥接管“抽象和实现两条体系各自扩展”:普通线路、大站快车可以继续发展,运营实现也可以单独增加。

单看一段代码,两者都可能是“一个对象持有接口引用,再委托调用”。区别主要不在这一行语法,而在设计意图和对象关系:策略通常围绕某个可替换算法,桥接则刻意保留两侧各自的层次。别指望靠一句“用了组合”把两者分开。

适配器模式管“把已有但不兼容的接口接起来”:目标接口要 ride(),旧类只有 takeTrain(),中间加一层转换。桥接更强调把两条变化维度主动拆开,让它们在统一约定下协作。

所以,北京地铁和京港地铁名称不同,并不需要适配器把 4 号线“转换”成 5 号线。真要出现已有调度系统接口对不上新线路接口,才轮到适配器登场。

Java 里常用 JDBC 来说明桥接思想:应用面对统一的 JDBC API,不同数据库由各自 Driver 提供实现,两侧可以分别发展。不过 JDBC 还包含工厂等其他设计,不适合被贴成“只有桥接”的标本。

06 什么时候,别急着拆成两边

**只有名称或配置不同,用字段。**像 LineInfo 那样能表达清楚,就别为“4 号线”“5 号线”各建一类。

**只有一边真正变化,先用简单接口。**如果线路类型固定,只是调度算法可替换,策略或普通依赖注入往往更直白,不必硬造两套抽象。

**两边始终一一对应,也别拆。**一种线路只有一种实现,而且没有独立扩展需求,桥接只会增加跳转。

判断标准可以落在一句话上:**这里是否确实有两条独立变化的维度,而把它们组合起来后,能避免成批复制“抽象 × 实现”的类?**是,桥接才有用武之地。

07 各走各的,也能一起把事办成

地铁继续往前开。小王只是觉得这些叫法挺有意思,至于换乘,还是看线路、看方向、看站名。

乘客不必先研究运营公司才能坐车,调用方也不必把所有实现细节塞进一个类。该拆开的拆开,该协作的协作。

跟前面十二篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展、协同工作。

带走一句话:桥接 = 用组合把抽象与实现分开,让两条变化维度独立扩展,再通过委托一起完成工作。

线路和运营方可以各走各的,乘客要去的,始终是下一站。

今天是周五。上完这一天,就好好过双休。列车往下一站开,周末也在前头等着了。


参考资料