GoF设计模式——桥接模式
本文是【GoF设计模式】系列第10篇前言为什么需要桥接模式假设要做一个图形编辑器有圆形、矩形、三角形三种图形每种图形又要支持红色、蓝色、黄色三种颜色。用继承来实现就要写RedCircle、BlueCircle、RedRectangle、BlueRectangle… 类的数量 图形数 × 颜色数3 × 3 9 个类。再加一种图形或一种颜色类就会爆炸式增长。// 继承方案类爆炸classRedCircleextendsCircle{...}classBlueCircleextendsCircle{...}classRedRectangleextendsRectangle{...}// ... 9 个类再加一个图形或颜色就要翻倍两种选择要么忍受类爆炸要么把颜色作为字段写在图形类里——后者看似简单但每种颜色逻辑都要在图形类内部判断代码会变得臃肿加一种颜色就要改所有图形类。这种两个维度各自变化组合后类爆炸的矛盾就是桥接模式要解决的问题。概念桥接模式Bridge Pattern是一种结构型设计模式核心思想是将抽象与实现分离到两个独立的维度使它们可以各自独立扩展。桥接模式包含四个角色Abstraction抽象定义抽象部分的接口持有对实现层的引用这就是桥。通常用抽象类因为需要持有引用并提供默认逻辑。RefinedAbstraction修正抽象对抽象接口进行扩展是抽象的具体变体。Implementor实现定义实现部分的接口与抽象层的接口可以完全不同。ConcreteImplementor具体实现实现实现化接口的具体类。继承持有桥实现实现«abstract»Abstraction-impl: Implementoroperation()RefinedAbstractionoperation()«interface»ImplementoroperationImpl()ConcreteImplementorAoperationImpl()ConcreteImplementorBoperationImpl()Abstraction 内部持有 Implementor 的引用这是桥的关键——通过组合而非继承连接两个维度。RefinedAbstraction 扩展抽象层ConcreteImplementorA/B 实现实现层两边各自独立发展。可以把桥接模式理解为遥控器和电视的关系遥控器是抽象层电视是实现层。遥控器内部持有一个电视的引用按下开机键时调用电视的开机方法。不同的遥控器基础遥控器、万能遥控器是修正抽象不同品牌的电视小米、索尼是具体实现。这样遥控器和电视可以各自独立发展——新增一个电视品牌不需要改遥控器的代码新增一种遥控器也不需要改电视的代码。实现桥接模式只有一种实现方式抽象层持有实现层的引用通过组合而非继承来关联两个维度。基础实现实现步骤定义实现层接口创建具体实现类定义抽象层基类抽象类内部持有实现层引用创建修正抽象类扩展抽象层。// 实现层接口 interfaceImplementor{publicvoidoperationImpl();}// 具体实现A classConcreteImplementorAimplementsImplementor{publicvoidoperationImpl(){System.out.println(实现方式A);}}// 具体实现B classConcreteImplementorBimplementsImplementor{publicvoidoperationImpl(){System.out.println(实现方式B);}}// 抽象层基类用抽象类因为需要持有实现层引用 abstractclassAbstraction{protectedImplementorimpl;// 桥持有实现层的引用publicAbstraction(Implementorimpl){this.implimpl;}publicvoidoperation(){impl.operationImpl();// 委托给实现层}}// 修正抽象 classRefinedAbstractionextendsAbstraction{publicRefinedAbstraction(Implementorimpl){super(impl);}publicvoidoperation(){// 可以在委托前后加自己的逻辑System.out.println(RefinedAbstraction 额外逻辑);super.operation();}}// 客户端 publicclassClient{publicstaticvoidmain(String[]args){ImplementorimplAnewConcreteImplementorA();AbstractionabsnewRefinedAbstraction(implA);abs.operation();}}引入一个例子「一台万能遥控器手里有小米电视和索尼电视两台设备。遥控器内部有个电视插槽插哪台电视就能控制哪台——遥控器本身不用改代码电视也不用改接口」。遥控器对应Abstraction抽象层电视对应Implementor实现层插槽对应构造方法注入的组合关系。换一台电视只需把新实例传给遥控器两边各自独立。// 实现层电视品牌interfaceTV{publicvoidpowerOn();publicvoidpowerOff();}classXiaomiTVimplementsTV{publicvoidpowerOn(){System.out.println(小米电视开机);}publicvoidpowerOff(){System.out.println(小米电视关机);}}classSonyTVimplementsTV{publicvoidpowerOn(){System.out.println(索尼电视开机);}publicvoidpowerOff(){System.out.println(索尼电视关机);}}// 抽象层遥控器abstractclassRemoteControl{protectedTVtv;// 持有电视的引用桥publicRemoteControl(TVtv){this.tvtv;}publicvoidturnOn(){tv.powerOn();}publicvoidturnOff(){tv.powerOff();}}// 修正抽象万能遥控器额外功能classUniversalRemoteextendsRemoteControl{publicUniversalRemote(TVtv){super(tv);}publicvoidmute(){System.out.println(静音);}}// 使用遥控器和电视自由组合RemoteControlbasicnewRemoteControl(newXiaomiTV());basic.turnOn();// 控制小米电视UniversalRemoteuniversalnewUniversalRemote(newSonyTV());universal.turnOn();universal.mute();// 万能遥控器额外功能为什么 Abstraction 用抽象类而不是接口桥接模式的抽象层需要持有实现层的引用接口不能有实例字段只有抽象类才能保存Implementor实例提供默认的委托逻辑operation()方法通常直接委托给impl写在抽象类中避免子类重复代码简单说接口定义能做什么抽象类定义怎么做框架。桥接模式的抽象层需要既有状态持有引用又有行为默认委托所以用抽象类更合适。桥接模式与简单组合的区别桥接模式本质上就是组合但它和普通的一个类持有另一个类有本质区别维度简单组合桥接模式目的复用功能、委托调用分离两个独立变化的维度结构无约束随意组合抽象层 实现层 桥持有引用扩展性通常只有一方可扩展两边各自独立扩展举个例子// 简单组合订单持有支付方式的引用classOrder{privatePaymentMethodpayment;// 组合只是委托publicvoidcheckout(){payment.pay(amount);}}订单和支付方式之间没有两个维度各自变化的关系——订单只有一个维度支付方式也只有一个维度。目的只是委托支付不是解决类爆炸。而桥接模式的图形 渲染// 桥接图形持有渲染器的引用abstractclassShape{protectedRendererrenderer;// 桥维度分离publicvoiddraw(){renderer.render(this);}}图形是一个维度圆形、矩形…渲染是另一个维度矢量、像素…两边各自独立扩展——新增一种图形不影响渲染层新增一种渲染不影响图形层。判断标准新增一个实现类抽象层是否需要新增对应的子类简单组合新增ApplePay订单类不动——一方扩展另一方不动桥接模式新增渲染方式图形类不动新增图形渲染类不动——两边都可以独立扩展记忆口诀组合是手段桥接是结构。组合解决复用桥接解决维度分离。总结桥接模式本质上是用组合替代继承来解决两个维度各自变化的问题——把乘法关系的类数量变成加法关系。什么时候用一个类有两个独立变化的维度如图形和颜色、支付方式和优惠策略每个维度都可能独立扩展不希望类数量爆炸需要在运行时动态切换实现层如换一个电视品牌什么时候不用只有一个变化维度用继承或简单组合就够了两个维度耦合紧密分开反而增加理解成本系统简单过早使用桥接模式会增加复杂度简单记忆桥接拆维度组合替继承。两个方向各自变类数从乘变加法。模式接口关系核心意图典型场景桥接抽象持有实现引用分离两个独立变化的维度图形渲染、消息渠道适配器目标接口 ≠ 被包装对象接口转换接口让不兼容的类协同第三方库集成策略上下文持有策略引用替换同一维度的不同算法排序算法、折扣计算抽象工厂工厂接口定义创建方法创建一系列相关对象跨数据库 DAO口诀对比桥接拆维度适配改接口策略换算法工厂管创建。桥接 vs 适配器维度桥接模式适配器模式核心意图事前设计分离两个独立变化的维度事后补救让不兼容的接口协同工作结构差异抽象层持有实现层引用双方接口可不同适配器实现目标接口持有被适配对象关注点预防类爆炸支持独立扩展解决接口不兼容问题典型场景JDBC 驱动、跨平台 UI第三方库集成、遗留系统对接逐步区分法如果两个维度都可能独立扩展 → 选桥接如果只是要让现有类协同工作 → 选适配器如果需要事后补救接口不兼容 → 选适配器桥接 vs 策略维度桥接模式策略模式核心意图分离两个独立变化的维度替换同一维度的不同算法结构差异抽象层持有实现层引用两者是平行的维度上下文持有策略引用策略是算法的变体关注点维度分离独立扩展算法替换行为可变典型场景图形渲染、消息渠道排序算法、折扣计算逐步区分法如果有两个独立变化的维度 → 选桥接如果只是同一个行为的不同实现方式 → 选策略如果关注点是用什么算法 → 选策略如果关注点是用什么实现体系 → 选桥接桥接 vs 抽象工厂维度桥接模式抽象工厂模式核心意图分离两个独立变化的维度创建一系列相关对象结构差异抽象层持有实现层引用工厂接口定义创建方法关注点结构分离运行时切换对象创建产品族约束典型场景JDBC 驱动、跨平台 UI跨数据库 DAO、跨 UI 主题逐步区分法如果需要在运行时切换实现 → 选桥接如果只需要创建一系列相关对象 → 选抽象工厂如果关注点是对象怎么创建 → 选抽象工厂如果关注点是实现怎么切换 → 选桥接练习题目图形渲染系统题目描述一个图形系统需要支持多种图形和多种渲染方式。图形包括圆形Circle和矩形Rectangle渲染方式包括矢量渲染Vector和像素渲染Pixel。请使用桥接模式实现使得图形类型和渲染方式可以独立扩展。输入描述第一行是一个整数 N1 ≤ N ≤ 100表示后面有 N 行输入。接下来的 N 行每行包含两个字符串第一个表示图形类型Circle / Rectangle第二个表示渲染方式Vector / Pixel。输出描述对于每行输入输出该图形使用该渲染方式的绘制结果。输入示例6 Circle Vector Circle Pixel Rectangle Vector Rectangle Pixel Circle Vector Rectangle Pixel输出示例Drawing Circle with Vector Renderer Drawing Circle with Pixel Renderer Drawing Rectangle with Vector Renderer Drawing Rectangle with Pixel Renderer Drawing Circle with Vector Renderer Drawing Rectangle with Pixel Renderer解题思路如果不使用桥接模式就要为每种组合创建类VectorCircle、PixelCircle、VectorRectangle、PixelRectangle类数量 图形数 × 渲染数。使用桥接模式渲染方式是实现层Renderer接口图形是抽象层Shape抽象类持有Renderer引用通过构造方法注入。这样图形和渲染各自独立扩展类数量从乘法变成加法。importjava.util.*;publicclassMain{publicstaticvoidmain(String[]args){ScannerscnewScanner(System.in);intnsc.nextInt();while(n--0){StringshapeTypesc.next();StringrendererTypesc.next();// 实现层渲染方式Rendererrnull;if(Vector.equals(rendererType)){rnewVector();}else{rnewPixel();}// 抽象层图形持有渲染器引用Shapesnull;if(Circle.equals(shapeType)){snewCircle(r);}else{snewRectangle(r);}s.draw();}}}// 实现层接口渲染器interfaceRenderer{publicStringgetName();}classVectorimplementsRenderer{publicStringgetName(){returnVector Renderer;}}classPixelimplementsRenderer{publicStringgetName(){returnPixel Renderer;}}// 抽象层图形持有渲染器引用abstractclassShape{protectedRendererrenderer;protectedStringname;publicShape(Stringname,Rendererrenderer){this.namename;this.rendererrenderer;}publicvoiddraw(){System.out.println(Drawing name with renderer.getName());}}classCircleextendsShape{publicCircle(Rendererr){super(Circle,r);}}classRectangleextendsShape{publicRectangle(Rendererr){super(Rectangle,r);}}扩展实际项目中的桥接模式JDBC 驱动架构JDBC 是桥接模式最经典的应用。Connection、Statement等接口是抽象层各数据库厂商的驱动MySQL Connector、PostgreSQL Driver是实现层。应用代码只依赖 JDBC 接口不关心底层是哪个数据库——MySQL 升级驱动版本不影响业务代码切换数据库只需换驱动和连接字符串。消息通知系统通知系统需要支持多种消息类型普通、紧急、营销和多种发送渠道短信、邮件、微信。用桥接模式消息类型是抽象层发送渠道是实现层。新增一种消息类型只需加一个Message子类新增一种发送渠道只需加一个MessageSender实现两者互不影响。日志框架的 AppenderSLF4J Logback 中Logger 是抽象层Appender输出目标是实现层。一个 Logger 可以配置多个 Appender日志同时输出到控制台、文件、远程服务。新增异步 Logger 不影响 Appender新增 Kafka Appender 不影响 Logger。跨平台 UI 组件UI 框架中按钮是一个维度操作系统是另一个维度。同一种按钮在不同操作系统上的渲染方式不同。用桥接模式UI 组件是抽象层操作系统渲染是实现层。新增下拉框不影响操作系统层新增 Linux 支持不影响 UI 组件层。支付方式与优惠策略电商系统中支付方式支付宝、微信是一个维度优惠策略无优惠、满减、折扣是另一个维度。用桥接模式订单是抽象层支付方式是实现层。新增 Apple Pay 不影响优惠策略新增首单立减不影响支付方式。现在可能还在写简单的业务代码但等到遇到两个维度都要扩展的场景时——与其让类数量爆炸不如用桥接模式把它们拆开各自独立发展。那时候就真的懂了。