设计模式之桥接模式

引言

当我们面对一个抽象的概念,并且这个概念有好几种不同的实现方式时,一般会用继承的办法来处理。我们会先创建一个抽象类,这个抽象类规定了这个抽象概念的一些基本功能和操作,也就是接口。然后,通过不同的具体子类,用各自独特的方式来实现这些功能和操作。但是因为继承会把抽象类和它的具体实现紧紧绑在一起,要是以后想单独修改抽象类里的功能,或者改变具体子类的实现方式,又或者想在其他地方复用这些代码,就会很麻烦。

想象一下生活中的家具世界,家具类型丰富多样,有沙发、餐桌、衣柜等等;风格也各式各样,像现代简约风格、欧式古典风格、中式风格等。在编程中中,我们先创建了一个抽象类 “家具”,基于它实现了各种具体的家具。同时,还创建了一个抽象类 “风格”,并实现了多种不同风格。

当我们要创建具体的家具产品时,就需要把家具类型和风格进行组合,于是就会出现现代简约风格沙发类、欧式古典风格沙发类、现代简约风格餐桌类、欧式古典风格餐桌类等等。

在这里插入图片描述

问题在于,一旦需要扩展家具类型或者风格类型,就必须相应地去组合新的风格和家具。随着家具类型和风格数量的不断增加,子类的数量会呈爆发式增长。打个比方,假设有 n 种家具类型,m 种风格,那么子类的数量就会多达 n * m 个。

所以继承机制在此时就暴露了三个核心问题:

  1. 维度爆炸*:假设现有5种家具类型(沙发/床/餐桌/书柜/电视柜)和4种风格(现代/欧式/美式/中式),需要创建5* * 4 = 20个具体子类。若新增一种新风格(如日式),就需要立即派生5个新子类,导致类数量暴增到25个。
  2. 强耦合风险:当抽象类需要添加或修改方法时,所有子类都必须被动修改。这种严格层级关系迫使所有风格的具体家具都必须适配同一接口变更,即使某些风格根本不需要高度调节功能。
  3. 复用障碍:假设已开发了一套智能家居控制模块,需适配所有现代风格的家具。由于现代沙发、现代餐桌等类都是继承链中的叶节点,无法将"现代风格"这一公共特征抽象为独立模块复用,只能在每个具体类中重复实现风格相关代码。

而对此类问题,桥接模式(Bridge),可能是一个比较好的处理方式。

概述

定义

桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立地变化。通过引入一个桥接(抽象化与实现化之间的关联),将两个维度的变化解耦,避免在多个维度上扩展时产生的类爆炸等问题。

这里的抽象部分并非指抽象类或接口,而是指与具体实现相关的抽象概念,而实现部分也不是指具体的代码实现,而是指实现抽象部分所需的具体行为或功能。

目的

  • 解耦抽象和实现:让抽象和实现可以沿着各自的维度独立变化,避免在抽象和实现之间建立静态的继承关系,使得系统更加灵活和可维护。
  • 提高可扩展性:当需要扩展抽象或实现的功能时,只需要在相应的部分进行修改或添加,而不会影响到另一部分,符合开闭原则。
  • 增强可复用性:抽象和实现的分离使得它们可以在不同的场景中被复用,提高了软件的复用性。

结构

  • 抽象化(Abstraction):定义抽象类,并包含一个对实现化对象的引用。它为客户端提供了与抽象相关的接口,并且可以包含一些默认的行为或属性。
  • 扩展抽象化(Refined Abstraction):是抽象化的具体子类,用于扩展抽象化的功能,通常会重写抽象化中的方法,以提供更具体的实现。
  • 实现化(Implementor):定义实现类的接口,这个接口不一定要与抽象化的接口完全一致,实际上,这两个接口可以完全不同。一般来说,实现化接口提供的是一些基本的操作,而抽象化接口则是基于这些基本操作来定义更高层次的操作。
  • 具体实现化(Concrete Implementor):是实现化接口的具体实现类,负责实现具体的业务逻辑。

工作原理

客户端通过抽象化角色来调用系统的功能,抽象化角色内部会调用实现化角色的方法来完成具体的操作。当需要改变系统的行为或功能时,可以通过替换具体的实现化角色或者扩展抽象化角色来实现,而不需要对整个系统进行大规模的修改。

示例

我们尝试使用桥接模式来解决上述引言中的例子。

  • 分离家具和风格维度
    • 家具维度:将家具类型作为抽象化部分。可以定义一个抽象的家具类,它包含家具的通用属性和行为,如重量、尺寸、移动和清洁等方法。然后,针对不同的家具类型,如沙发、餐桌、衣柜等,创建各自的具体家具类作为扩展抽象化,它们继承自抽象家具类,并实现各自特有的行为。
    • 风格维度:将风格作为实现化部分。定义一个风格接口,它包含与风格相关的方法,比如获取风格特点、应用风格装饰等。然后,针对不同的风格,如现代简约风格、欧式古典风格、中式风格等,创建具体的风格类来实现风格接口,实现各自风格特有的逻辑。
  • 建立桥接关系
    • 在抽象家具类中,引入一个风格接口类型的成员变量,通过构造函数或 setter 方法将具体的风格对象传入。这样,抽象家具类及其子类就可以通过这个成员变量调用风格对象的方法,实现不同风格在不同家具上的应用。
  • 解决问题的优势
    • 可扩展性增强:当需要添加新的家具类型时,只需创建一个新的具体家具类,继承抽象家具类即可,不需要为每种已有的风格都创建一个新的子类。同样,当添加新的风格时,只需创建一个新的具体风格类实现风格接口,而不需要为每种家具都创建一个新的风格子类。
    • 维护性提高:由于家具和风格的变化被分离到不同的层次中,当修改家具的属性或行为时,只需要在相应的家具类中进行修改;当修改风格的逻辑时,只需要在相应的风格类中进行修改,不会像只使用继承那样,一处修改可能导致大量子类需要修改。
    • 代码复用性提升:家具类和风格类都可以被独立地复用。不同的家具类型可以复用相同的风格类,不同的风格也可以应用到不同的家具类型上,提高了代码的复用性,减少了代码冗余。

使用代码实现如下:

UML图

在这里插入图片描述

C++实现:

#include <iostream>
#include <string>
#include <memory>

// 实现化角色:风格接口
class Style {
public:
    virtual ~Style() = default;
    [[nodiscard]] virtual std::string getStyleFeatures() const = 0;
    virtual void applyStyleDecoration() const = 0;
};


// 具体实现化角色:现代简约风格
class ModernStyle : public Style {
public:
    [[nodiscard]] std::string getStyleFeatures() const override {
        return "现代简约风格,简约的线条设计,纯色外观";
    }
    void applyStyleDecoration() const override {
        std::cout << "应用现代简约风格的装饰" << std::endl;
    }
};


// 具体实现化角色:欧式古典风格
class EuropeanClassicalStyle : public Style {
public:
    [[nodiscard]] std::string getStyleFeatures() const override {
        return "欧式古典风格,雕花工艺,复古颜色";
    }
    void applyStyleDecoration() const override {
        std::cout << "应用欧式古典风格的装饰" << std::endl;
    }
};


// 抽象化角色:家具抽象类
class Furniture {
protected:
    std::unique_ptr<Style> style;
public:
    explicit Furniture(std::unique_ptr<Style> s) : style(std::move(s)) {}
    virtual ~Furniture() = default;
    virtual void displayInfo() const = 0;
    void applyStyle() const {
        style->applyStyleDecoration();
    }
};


// 扩展抽象化角色:沙发类
class Sofa : public Furniture {
public:
    explicit Sofa(std::unique_ptr<Style> s) : Furniture(std::move(s)) {}
    void displayInfo() const override {
        std::cout << "这是一个沙发,具有 " << style->getStyleFeatures() << std::endl;
    }
};


// 扩展抽象化角色:餐桌类
class DiningTable : public Furniture {
public:
    explicit DiningTable(std::unique_ptr<Style> s) : Furniture(std::move(s)) {}
    void displayInfo() const override {
        std::cout << "这是一张餐桌,具有 " << style->getStyleFeatures() << std::endl;
    }
};


int main() {
    // 创建现代简约风格对象
    auto modernStyle = std::make_unique<ModernStyle>();
    // 创建具有现代简约风格的沙发
    auto modernSofa = std::make_unique<Sofa>(std::move(modernStyle));
    modernSofa->displayInfo();
    modernSofa->applyStyle();
    std::cout << std::endl;
    // 创建欧式古典风格对象
    auto europeanClassicalStyle = std::make_unique<EuropeanClassicalStyle>();
    // 创建具有欧式古典风格的餐桌
    auto europeanClassicalDiningTable = std::make_unique<DiningTable>(std::move(europeanClassicalStyle));
    europeanClassicalDiningTable->displayInfo();
    europeanClassicalDiningTable->applyStyle();
    return 0;
}

Java实现

// 实现化角色:风格接口
interface Style {
    String getStyleFeatures();
    void applyStyleDecoration();
}

// 具体实现化角色:现代简约风格
class ModernStyle implements Style {
    @Override
    public String getStyleFeatures() {
        return "现代简约风格,简约的线条设计,纯色外观";
    }
    @Override
    public void applyStyleDecoration() {
        System.out.println("应用现代简约风格的装饰");
    }
}

// 具体实现化角色:欧式古典风格
class EuropeanClassicalStyle implements Style {
    @Override
    public String getStyleFeatures() {
        return "欧式古典风格,雕花工艺,复古颜色";
    }
    @Override
    public void applyStyleDecoration() {
        System.out.println("应用欧式古典风格的装饰");
    }
}

// 抽象化角色:家具抽象类
abstract class Furniture {
    protected Style style;
    public Furniture(Style style) {
        this.style = style;
    }
    public abstract void displayInfo();
    public void applyStyle() {
        style.applyStyleDecoration();
    }
}

// 扩展抽象化角色:沙发类
class Sofa extends Furniture {
    public Sofa(Style style) {
        super(style);
    }
    @Override
    public void displayInfo() {
        System.out.println("这是一个沙发,具有 " + style.getStyleFeatures());
    }
}

// 扩展抽象化角色:餐桌类
class DiningTable extends Furniture {
    public DiningTable(Style style) {
        super(style);
    }
    @Override
    public void displayInfo() {
        System.out.println("这是一张餐桌,具有 " + style.getStyleFeatures());
    }
}


// 主程序
public class BridgePatternExample {
    public static void main(String[] args) {
        // 创建现代简约风格对象
        Style modernStyle = new ModernStyle();
        // 创建具有现代简约风格的沙发
        Furniture modernSofa = new Sofa(modernStyle);
        modernSofa.displayInfo();
        modernSofa.applyStyle();
        System.out.println();
        // 创建欧式古典风格对象
        Style europeanClassicalStyle = new EuropeanClassicalStyle();
        // 创建具有欧式古典风格的餐桌
        Furniture europeanClassicalDiningTable = new DiningTable(europeanClassicalStyle);
        europeanClassicalDiningTable.displayInfo();
        europeanClassicalDiningTable.applyStyle();
    }
}

代码解释

  1. 实现化角色(Style及具体子类)
    • Style是一个抽象基类,定义了风格的接口,包含getStyleFeaturesapplyStyleDecoration两个纯虚函数。
    • ModernStyleEuropeanClassicalStyle是具体的风格类,继承自Style,并实现了getStyleFeaturesapplyStyleDecoration方法,分别描述了现代简约风格和欧式古典风格的特点及装饰应用。
  2. 抽象化角色(Furniture
    • Furniture是家具的抽象类,包含一个指向Style对象的指针,通过构造函数接收具体的风格对象。
    • displayInfo是纯虚函数,需要具体的家具子类实现。
    • applyStyle方法调用风格对象的applyStyleDecoration方法,用于应用具体风格的装饰。
  3. 扩展抽象化角色(SofaDiningTable
    • SofaDiningTable是具体的家具类,继承自Furniture,并实现了displayInfo方法,用于显示家具的信息和所具有的风格特点。

总结

在家具与风格的案例里,桥接模式巧妙地把家具类型和风格这两个会不断变化的维度区分开来。一方面,抽象出家具类,这个类里持有一个风格接口的引用。具体的家具子类,比如沙发类、餐桌类等,继承自这个抽象家具类,并且在创建对象时能够接收不同的风格对象。另一方面,风格通过一个接口来定义,不同的具体风格类,像现代简约风格类、欧式古典风格类等,负责实现这个接口。

当创建具体家具子类的对象时,会在构造函数中将具体的风格对象作为参数传入,这样就把抽象的家具类和具体的风格类联系起来了。这一设计带来的好处是,我们可以独立地对家具类型和风格进行扩展。比如要新增一种家具类型或者一种风格,都不会对其他部分造成过多影响。

从底层原理来讲,桥接模式借助多态性发挥作用。抽象的家具类通过持有的风格接口引用,在程序运行的时候可以调用不同风格类的具体方法。这样一来,抽象的家具部分和具体的风格实现部分就实现了解耦,避免了单纯使用继承所导致的类数量急剧增加的问题。它采用组合的方式,而非继承,让家具和风格能够灵活搭配。

桥接模式的关键在于,先把抽象和实现这两个维度分离开,然后利用多态和组合来建立它们之间的关联。如此,这两个维度就能各自独立变化,极大地提升了系统的可扩展性和可维护性,让系统在面对变化时更加灵活、稳定。

桥接模式的优缺点

优点

1. 分离抽象和实现,提高可扩展性

桥接模式将抽象部分与实现部分分离,使它们可以独立地变化。在之前的家具与风格示例中,家具类型(抽象部分)和风格(实现部分)能够分别进行扩展。当需要新增一种家具类型,如书架,或者新增一种风格,如地中海风格时,只需要在对应的维度上创建新的类,而不会影响到另一个维度的代码,大大提高了系统的可扩展性。

2. 避免类爆炸问题

如果不使用桥接模式,采用传统的继承方式,为每一种家具类型和风格的组合创建一个类,那么随着家具类型和风格数量的增加,类的数量会呈指数级增长,导致代码难以维护和管理。而桥接模式通过组合的方式,避免了这种类爆炸的情况,减少了类的数量,使代码结构更加清晰。

3. 提高代码复用性

抽象部分和实现部分可以独立复用。不同的抽象类可以使用相同的实现类,反之亦然。例如,现代简约风格可以应用到沙发、餐桌等多种家具上,同一种家具也可以搭配不同的风格,提高了代码的复用率,减少了代码冗余。

4. 符合开闭原则

开闭原则强调对扩展开放,对修改关闭。桥接模式使得系统在扩展功能时,只需要添加新的类,而不需要修改现有的代码,符合开闭原则,增强了系统的稳定性和可维护性。

缺点

1. 增加系统复杂性

桥接模式引入了抽象和实现两个维度,并且通过组合的方式建立它们之间的关联,这会增加系统的理解难度和设计复杂度。对于简单的系统来说,使用桥接模式可能会使代码变得过于复杂,得不偿失。

2. 调试和维护难度增加

由于桥接模式将抽象和实现分离,当出现问题时,需要同时考虑抽象部分和实现部分的代码,调试和定位问题的难度会相对增加。特别是在复杂的系统中,不同的抽象和实现组合可能会导致问题的排查变得更加困难。

3. 初始设计难度较大

在使用桥接模式进行系统设计时,需要准确地识别出系统中的变化维度,并将其分离为抽象和实现部分。这对于设计者的经验和能力要求较高,如果设计不当,可能无法充分发挥桥接模式的优势,甚至会导致系统结构混乱。

注意事项

设计阶段

1. 准确识别变化维度

桥接模式的核心在于分离抽象和实现,所以要精准找出系统中可能独立变化的维度。比如在家具与风格的例子里,家具类型和风格就是两个明显的变化维度。若识别不准确,可能会错误划分抽象和实现部分,无法发挥桥接模式的优势,甚至让系统结构更复杂。

2. 合理设计抽象和实现接口
  • 抽象接口:要定义通用且稳定的行为,为具体的抽象类提供清晰的高层接口。接口不能过于复杂,避免给子类实现带来困难;也不能过于简单,否则无法满足实际需求。
  • 实现接口:要专注于实现细节,为具体的实现类提供明确的操作规范。接口应具有良好的扩展性,方便后续添加新的实现类。
3. 考虑系统规模和复杂度

桥接模式适合处理具有多个变化维度且系统规模较大、复杂度较高的场景。对于简单的系统,使用桥接模式可能会增加不必要的复杂性。所以在使用前要评估系统规模和未来的扩展需求,判断是否真正需要使用桥接模式。

编码阶段

1. 确保抽象和实现的独立性

在代码实现中,要保证抽象部分和实现部分能够独立变化。抽象类不应该依赖于具体的实现类,而应该依赖于实现接口。通过组合而非继承的方式建立两者的联系,避免抽象和实现的强耦合。

2. 注意内存管理

如果在桥接模式中使用了动态内存分配(如在 C++ 中使用new操作符),要特别注意内存的释放,避免内存泄漏。通常可以在抽象类的析构函数中负责释放实现对象的内存。

3. 做好错误处理

由于桥接模式涉及抽象和实现的交互,在调用实现部分的方法时可能会出现异常。因此,要在代码中做好错误处理,确保系统的健壮性。例如,在抽象类调用实现接口的方法时,要对可能出现的异常进行捕获和处理。

维护和扩展阶段

1. 遵循开闭原则

在对系统进行扩展时,要遵循开闭原则,即对扩展开放,对修改关闭。当需要添加新的抽象类或实现类时,应该尽量避免修改现有的代码,而是通过添加新的类来实现功能扩展。

2. 文档和注释

为了方便后续的维护和扩展,要为代码添加详细的文档和注释。说明抽象和实现部分的职责、接口的使用方法以及类之间的关系,让其他开发者能够快速理解代码结构和功能。

3. 测试和验证

在每次进行系统扩展或修改后,要进行充分的测试和验证,确保新添加的功能不会影响原有的功能,并且抽象和实现之间的交互仍然正常。可以使用单元测试、集成测试等多种测试方法来保证系统的质量。

应用场景

存在多个变化维度的系统

  • 图形绘制系统:在图形绘制软件里,图形类型(如圆形、矩形、三角形等)和绘制方式(如在屏幕上绘制、在打印机上绘制、在文件中保存为图形格式等)是两个不同的变化维度。使用桥接模式可以将图形类型作为抽象部分,绘制方式作为实现部分。这样,当需要新增一种图形或者一种绘制方式时,只需分别在对应的维度上进行扩展,而不会相互影响,提高了系统的可扩展性。
  • 电商系统:商品有不同的品类(如服装、电子产品、食品等),同时商品的销售渠道也多种多样(如官网、第三方电商平台、线下门店等)。桥接模式可以把商品品类作为抽象部分,销售渠道作为实现部分。当要增加新的商品品类或者拓展新的销售渠道时,能够独立进行开发和调整,避免类的数量过度膨胀。

需要提高系统可扩展性和可维护性的场景

  • 游戏开发:游戏中的角色和技能是可以独立变化的。角色有不同的类型(如战士、法师、刺客等),技能也有多种(如攻击技能、防御技能、辅助技能等)。使用桥接模式,将角色类型抽象出来,技能作为实现部分。这样在后续开发中,无论是新增角色还是设计新技能,都能方便地进行扩展,并且由于抽象和实现分离,代码的维护也更加容易,减少了修改一处代码影响其他部分的风险。
  • 企业级应用系统:这类系统通常涉及多种业务逻辑和数据存储方式。例如,业务模块可能包括客户管理、订单管理、库存管理等,数据存储方式有数据库存储、文件存储、云存储等。通过桥接模式将业务模块作为抽象部分,数据存储方式作为实现部分,能够让系统在面对业务需求变化或者存储技术升级时,灵活应对,降低维护成本。

避免使用多重继承的情况

  • 编程语言限制:有些编程语言不支持多重继承(如 Java),或者多重继承会带来复杂的菱形继承问题(如 C++)。桥接模式可以通过组合的方式替代多重继承,实现不同功能的组合。例如,一个软件系统中的某个组件需要同时具备日志记录和权限验证功能,使用桥接模式可以将日志记录和权限验证分别作为实现部分,通过组合到抽象组件类中,避免了多重继承可能带来的问题。
  • 代码复杂度控制:即使编程语言支持多重继承,当系统中存在多个维度的变化时,使用多重继承会使类的层次结构变得非常复杂,难以理解和维护。桥接模式通过清晰地分离抽象和实现,让代码结构更加清晰,易于管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值