设计模式之适配器模式

引言

狂风呼啸,一场强台风正以迅猛之势逼近你所在的城市,带来极大的威胁。而祸不单行,市中心的一座大楼突发火灾,情况万分危急。应急指挥中心里气氛凝重,领导紧盯着屏幕,一边是 GIS 系统中由气象部门实时更新的降雨量、风速数据以及精准的地图信息,这些数据对于掌握台风的动态和影响范围至关重要;另一边则是 CAD 系统中结构工程师精心标注的建筑承重参数,这是评估大楼及周边建筑安全状况的关键依据。
领导需要在极短时间内,综合这些信息快速划定群众疏散范围,同时精确预估周边建筑的受损风险。然而,现实却异常棘手,由于 CAD 和 GIS 系统的数据存在巨大差异,导致二者难以协同工作。CAD 采用的工程坐标系与 GIS 的地理坐标系完全不匹配,根本无法直接进行叠加分析;建筑属性字段与空间分析接口也互不相容,就像两个交流不畅的人,难以达成有效的合作。
这种数据上的割裂让领导不得不频繁在多个平台之间手动转换数据进行计算,每一次操作都繁琐又耗时。每一秒的流逝都可能关乎群众的安危,宝贵的应急响应时间就在这一次次的数据转换中悄然溜走。时间愈发紧迫,领导一边沉稳地发布救援指挥命令,一边忍不住焦急怒吼:“什么时候我们的应急灾害管理系统能高效整合这些数据!”
就在这千钧一发之际,英俊无敌的你挺身而出,提出建议:“我们开发一个适配器来整合这些数据吧!” 适配器就如同为不同型号电源插头量身定制的万能转换器,通过巧妙搭建中间层,能够成功消除系统间的差异。一旦实现,CAD 中的建筑轮廓便能精准、动态地投射到 GIS 的三维地形模型上,钢筋混凝土的力学参数也能与洪水淹没算法等各类灾害分析模型无缝对接,在未来还能为接入卫星遥感、物联网传感等新数据源预留架构弹性,灾害推演的准确性将实现质的飞跃,为救援决策提供更有力的支持。

故事纯属虚构,对于一些需要实时性的功能模块使用适配器模式可能并不合适,实际开发请结合实际选择设计模式。

所以,让我们一起来了解一下适配器模式吧!

概述

适配器模式(Adapter Pattern)是一种结构型设计模式1,它主要用于将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类能够协同工作。

定义

适配器模式就像是一个转换器,它能够把一个类的接口适配成另一个接口,让不兼容的接口之间能够进行通信和协作,以满足特定的业务需求。

作用

  • 解决接口不兼容问题:在软件系统中,常常会出现两个或多个类之间的接口不匹配的情况。例如,一个新的模块可能需要使用一个现有的类,但现有的类的接口与新模块所期望的接口不一致。适配器模式可以将现有的类的接口转换为新模块能够使用的接口,使得它们能够顺利协作。
  • 提高代码的复用性:通过适配器模式,可以将一些现有的类适配到不同的场景中使用,而不需要对这些现有的类进行大量的修改。这样可以充分利用已有的代码资源,提高代码的复用性,减少开发成本和维护成本。
  • 增强系统的灵活性和可扩展性:当系统中出现新的接口需求或者现有接口发生变化时,适配器模式可以在不影响其他模块的情况下,通过创建新的适配器类来进行适配,使得系统具有更好的灵活性和可扩展性。

相关角色

  • 目标接口(Target):定义了客户所期望的接口,也就是适配器最终要转换成的接口。
  • 适配器(Adapter):适配器模式的核心角色,它实现了目标接口,并且持有一个被适配者的引用。它负责将被适配者的接口转换为目标接口。
  • 被适配者(Adaptee):需要被适配的类,它有自己的接口,但这个接口与目标接口不兼容。

类型

  • 类适配器:通过继承来实现适配器功能,适配器类继承自被适配者类,同时实现目标接口。这种方式在 Java 等编程语言中使用较多,它的优点是实现简单,只需要继承和实现接口即可;缺点是由于 Java 等语言不支持多继承,如果被适配者已经有了父类,就无法使用类适配器。
  • 对象适配器:通过组合的方式来实现适配器功能,适配器类持有一个被适配者的对象引用,然后在实现目标接口的方法中调用被适配者的相应方法。这种方式更加灵活,在大多数情况下推荐使用。因为它可以在不改变被适配者类的情况下,将不同的被适配者适配到目标接口,而且可以方便地对被适配者的行为进行扩展和修改。

工作流程

  1. 初始化阶段
    • 创建目标接口对象:在客户端代码中,首先会根据需求创建一个目标接口类型的对象。这个对象代表了客户端所期望的接口,它定义了客户端可以调用的方法。
    • 创建被适配者对象:同时,需要创建被适配者类的对象。被适配者是具有与目标接口不兼容接口的类,它有自己的方法和行为。
    • 创建适配器对象并关联被适配者:创建适配器类的对象,并在创建过程中将被适配者对象传递给适配器。通常通过适配器类的构造函数来实现,这样适配器就持有了对被适配者的引用,以便后续调用被适配者的方法。
  2. 调用适配方法阶段
    • 客户端调用目标接口方法:客户端代码通过目标接口对象调用目标接口中定义的方法,这个方法是客户端期望使用的方法,它与客户端的业务逻辑紧密相关。
  3. 适配转换阶段
    • 适配器接收调用并转换请求:适配器接收到客户端对目标接口方法的调用请求后,会根据具体的适配逻辑,将这个请求转换为对被适配者相应方法的调用。这可能涉及到参数的转换、方法的映射等操作。例如,目标接口方法的参数是某种特定类型,而被适配者的方法接受的是另一种类型,适配器就需要将参数从目标接口的类型转换为被适配者方法所需的类型。
    • 调用被适配者方法:适配器完成请求的转换后,调用被适配者对象的相应方法,将转换后的请求传递给被适配者,让被适配者执行实际的操作。
  4. 结果返回阶段
    • 被适配者执行操作并返回结果:被适配者执行方法后,会返回一个结果。这个结果是按照被适配者的接口和实现逻辑生成的。
    • 适配器接收结果并转换返回值:适配器接收到被适配者返回的结果后,可能需要对结果进行进一步的转换,使其符合目标接口方法的返回值类型和要求。
    • 适配器将结果返回给客户端:最后,适配器将经过转换后的结果返回给客户端,客户端就可以得到符合目标接口定义的结果,并继续进行后续的业务逻辑处理。

示例

下面,我们以来自不同图形库的图形类编写适配器为例子编写示例代码。

UML图

在这里插入图片描述

C++实现

来自不同库的图形类

// 来自 图形库1 的圆形类
namespace Lib1Graphics {
    class Circle {
    private:
        double radius;
    public:
        explicit Circle(double r) : radius(r) {}
        // 图形库1绘制圆形方法
        void Lib1DrawCircle() const {
            std::cout << "Drawing a circle with radius " << radius << " from Lib1Graphics library." << std::endl;
        }
    };
}
// 来自 图形库2 的矩形类
namespace Lib2Graphics {
    class Rectangle {
    private:
        double width;
        double height;
    public:
        Rectangle(double w, double h) : width(w), height(h) {}
        // 图形库2绘制矩形方法
        void Lib2Rectangle() const {
            std::cout << "Drawing a rectangle with width " << width << " and height " << height << " from Lib2Graphics library." << std::endl;
        }
    };
}

适配器

#include <iostream>
#include <memory>
#include <vector>

// 目标接口,定义统一的绘制图形方法
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};
// 圆形适配器类
class CircleAdapter : public Shape {
private:
    const Lib1Graphics::Circle& circle;
public:
    explicit CircleAdapter(const Lib1Graphics::Circle& c) : circle(c) {}
    // 实现目标接口的 draw 方法,调用老的绘制圆形方法
    void draw() override {

        circle.Lib1DrawCircle();
    }
};
// 矩形适配器类
class RectangleAdapter : public Shape {
private:
    const Lib2Graphics::Rectangle& rectangle;
public:
    explicit RectangleAdapter(const Lib2Graphics::Rectangle& r) : rectangle(r) {}
    // 实现目标接口的 draw 方法,调用新的绘制矩形方法
    void draw() override {
        rectangle.Lib2Rectangle();
    }
};
int main() {
    // 创建不同来源的图形对象
    Lib1Graphics::Circle oldCircle(5.0);
    Lib2Graphics::Rectangle newRectangle(3.0, 4.0);
    // 创建适配器对象
    CircleAdapter circleAdapter(oldCircle);
    RectangleAdapter rectangleAdapter(newRectangle);
    // 使用 std::vector 管理 Shape 对象
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<CircleAdapter>(oldCircle));
    shapes.push_back(std::make_unique<RectangleAdapter>(newRectangle));
    // 以统一的接口调用绘制方法
    for (const auto& shape : shapes) {
        shape->draw();
    }
    return 0;
}

Java实现

// 来自 Lib1Graphics 的圆形类
class Lib1Circle {
    private double radius;
    public Lib1Circle(double radius) {
        this.radius = radius;
    }
    // 图形库1绘制圆形方法
    public void lib1DrawCircle() {
        System.out.println("Drawing a circle with radius " + radius + " from Lib1Graphics library.");
    }
}
// 来自 Lib2Graphics 的矩形类
class Lib2Rectangle {
    private double width;
    private double height;
    public Lib2Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    // 图形库2绘制矩形方法
    public void lib2DrawRectangle() {
        System.out.println("Drawing a rectangle with width " + width + " and height " + height + " from Lib2Graphics library.");
    }
}
// 目标接口,定义统一的绘制图形方法
interface Shape {
    void draw();
}
// 圆形适配器类
class CircleAdapter implements Shape {
    private Lib1Circle circle;
    public CircleAdapter(Lib1Circle circle) {
        this.circle = circle;
    }
    // 实现目标接口的 draw 方法,调用老的绘制圆形方法
    @Override
    public void draw() {
        circle.lib1DrawCircle();
    }
}
// 矩形适配器类
class RectangleAdapter implements Shape {
    private Lib2Rectangle rectangle;
    public RectangleAdapter(Lib2Rectangle rectangle) {
        this.rectangle = rectangle;
    }
    // 实现目标接口的 draw 方法,调用新的绘制矩形方法
    @Override
    public void draw() {
        rectangle.lib2DrawRectangle();
    }
}
public class Main {
    public static void main(String[] args) {
        // 创建不同来源的图形对象
        Lib1Circle lib1Circle = new Lib1Circle(5.0);
        Lib2Rectangle lib2Rectangle = new Lib2Rectangle(3.0, 4.0);
        // 创建适配器对象
        Shape circleAdapter = new CircleAdapter(lib1Circle);
        Shape rectangleAdapter = new RectangleAdapter(lib2Rectangle);
        // 使用 List 管理 Shape 对象
        List<Shape> shapes = new ArrayList<>();
        shapes.add(circleAdapter);
        shapes.add(rectangleAdapter);
        // 以统一的接口调用绘制方法
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

代码解释

  • Shape****(目标接口):定义了一个抽象方法draw(),它是客户端期望使用的统一接口。
  • CircleAdapterRectangleAdapter****(适配器):这两个类继承自Shape接口,实现了draw()方法。同时,它们分别持有Lib1Graphics::CircleLib2Graphics::Rectangle的引用,在draw()方法中调用被适配者的特定绘制方法。
  • Lib1Graphics::CircleLib2Graphics::Rectangle****(被适配者):它们是来自不同图形库的类,有各自独立的绘制方法,通过适配器类与Shape接口建立联系。

适配器模式的优缺点

优点

1. 提高代码复用性

适配器模式可以让现有的类在不改变其代码的情况下被复用。当需要使用一个已经存在的类,但其接口与当前系统不兼容时,通过创建适配器类,可以将这个类适配到新的接口上,从而避免了重新编写大量的代码。例如,在一个旧系统中已经有一个功能完善的算法类,但新系统需要的接口与该类的接口不同,此时可以创建一个适配器类来复用这个算法类。

2. 增强系统的灵活性和可扩展性

通过适配器模式,可以方便地对系统进行扩展。当有新的类需要集成到系统中,而其接口与现有系统不兼容时,只需要创建一个新的适配器类即可,不会影响到系统的其他部分。这使得系统能够更容易地适应变化,应对新的需求。例如,在一个图形绘制系统中,随着新的图形库的引入,只需要为新图形库创建适配器,就可以将其集成到现有系统中。

3. 解耦作用

适配器模式可以将客户端代码和被适配的类解耦。客户端只需要与目标接口进行交互,而不需要了解被适配类的具体实现细节。这样,当被适配类发生变化时,只要适配器类能够正确地进行适配,客户端代码就不需要进行修改。例如,在一个数据处理系统中,客户端只关心数据处理的结果,而不关心具体的数据来源和处理方式,适配器可以将不同来源的数据适配到统一的接口上,实现客户端与数据来源的解耦。

4. 符合开闭原则

开闭原则强调对扩展开放,对修改关闭。适配器模式很好地遵循了这一原则。在系统需要引入新的接口或类时,可以通过创建新的适配器类来实现,而不需要修改现有的客户端代码和被适配类的代码。例如,在一个电商系统中,随着支付方式的不断增加,只需要为新的支付方式创建适配器,就可以将其集成到系统中,而不需要修改原有的业务逻辑代码。

缺点

1. 增加系统复杂性

适配器模式引入了额外的适配器类,这会增加系统的代码量和复杂性。随着适配器数量的增加,系统的结构可能会变得更加复杂,理解和维护的难度也会相应提高。例如,在一个大型系统中,如果存在多个不同类型的适配器,可能会让开发人员在查找和理解代码时感到困惑。

2. 性能开销

在适配器模式中,适配器类需要进行接口转换和数据处理,这可能会带来一定的性能开销。尤其是在需要频繁进行适配操作的场景下,性能问题可能会更加明显。例如,在一个对性能要求极高的实时系统中,适配器的转换操作可能会影响系统的响应速度。

3. 过多使用可能导致设计混乱

如果在系统中过度使用适配器模式,可能会导致设计上的混乱。适配器模式本质上是一种补救措施,用于解决接口不兼容的问题。如果在设计阶段没有充分考虑接口的兼容性,而过多地依赖适配器来解决问题,会使系统的架构变得不清晰,难以把握系统的整体设计意图。例如,在一个小型项目中,如果频繁使用适配器来拼凑不同的功能模块,可能会导致项目的结构变得松散,难以维护和扩展。

注意事项

设计层面

1. 明确适配场景
  • 适配器模式主要用于解决接口不兼容问题,所以在使用前要确保确实存在接口不匹配的情况,避免过度使用。比如在系统开发中,只有当新引入的组件或模块接口与现有系统接口不一致,且修改原有接口成本过高时,才考虑使用适配器模式。
  • 若接口差异较小,可通过简单的代码调整解决,就无需引入适配器,以免增加系统复杂度。
2. 合理选择适配器类型
  • 类适配器:基于继承实现,适用于被适配类的接口较少,且希望在适配器中重写部分被适配类方法的场景。但由于大多数编程语言不支持多继承,若被适配类已有父类,就无法使用类适配器。
  • 对象适配器:基于组合实现,更为灵活,可适配多个不同的被适配类,且不会受继承的限制。在多数情况下,推荐优先使用对象适配器。
3. 遵循设计原则
  • 开闭原则:适配器模式应尽量遵循开闭原则,即对扩展开放,对修改关闭。当需要适配新的类或接口时,应通过创建新的适配器类来实现,而不是修改已有的适配器类。
  • 里氏替换原则:适配器类作为目标接口的实现类,应能够完全替代目标接口,确保在使用适配器的地方可以像使用目标接口一样正常工作。

代码实现层面

1. 清晰的接口转换逻辑
  • 适配器类中的接口转换逻辑要清晰明了,确保能够准确地将被适配者的接口转换为目标接口。在实现转换逻辑时,要考虑参数的映射、返回值的处理等细节。例如,当目标接口的方法参数类型与被适配者的方法参数类型不同时,需要进行合理的类型转换。
2. 避免过度适配
  • 适配器应仅负责接口转换,避免在适配器中添加过多的业务逻辑。如果适配器承担了过多的业务处理,会导致适配器类变得臃肿,违背了单一职责原则。
  • 例如,适配器只应完成将被适配者的方法调用转换为目标接口的方法调用,而不应该包含数据处理、业务规则判断等额外的业务逻辑。
3. 异常处理
  • 在适配器类中,要对可能出现的异常进行合理处理。由于适配器需要调用被适配者的方法,被适配者的方法可能会抛出异常,适配器需要捕获并处理这些异常,或者将异常封装成目标接口可以处理的异常类型抛出。
  • 例如,当被适配者的方法可能会因为网络问题抛出异常时,适配器可以将其封装成目标接口定义的网络异常类型,方便客户端统一处理。

性能和维护层面

1. 性能考量
  • 适配器的转换操作可能会带来一定的性能开销,特别是在需要频繁进行适配操作的场景下。因此,在设计适配器时,要尽量优化转换逻辑,减少不必要的计算和资源消耗。
  • 例如,避免在适配器中进行复杂的数据拷贝和转换操作,可采用缓存机制来提高性能。
2. 可维护性和扩展性
  • 为了便于后续的维护和扩展,适配器类的代码要具有良好的可读性和可维护性。可以添加必要的注释,遵循统一的编码规范。
  • 同时,要考虑到未来可能需要适配新的类或接口,设计适配器时应预留一定的扩展点,方便后续添加新的适配逻辑。例如,可以使用抽象工厂模式来创建适配器,便于根据不同的需求创建不同类型的适配器。
3. 文档和测试
  • 编写详细的文档,说明适配器的使用方法、适配的接口和转换逻辑等信息,方便其他开发者理解和使用。
  • 对适配器进行充分的测试,确保其能够正确地完成接口转换,并且在各种异常情况下都能稳定工作。测试用例应覆盖正常情况和边界情况,保证适配器的可靠性。

应用场景

1. 整合旧系统与新系统

  • 在软件系统的升级和改造过程中,经常会遇到旧系统的接口与新系统不兼容的情况。使用适配器模式可以在不修改旧系统代码的前提下,将旧系统的功能集成到新系统中。
  • 例如,某公司有一个使用多年的旧库存管理系统,其数据接口是基于 XML 格式的。现在公司要开发一个新的电商系统,该系统采用 JSON 格式的数据接口。为了让新系统能够使用旧库存管理系统的数据,可以创建一个适配器,将 XML 数据转换为 JSON 数据,实现两个系统的无缝对接。

2. 复用第三方库或组件

  • 当引入第三方库或组件时,其接口可能与我们的系统接口不匹配。适配器模式可以帮助我们将第三方库的接口适配到我们的系统中,从而复用这些现成的功能。
  • 比如,我们正在开发一个图形绘制应用程序,需要使用一个第三方的图形处理库,但该库的绘制方法与我们系统中定义的绘制接口不同。此时,可以创建一个适配器类,将第三方库的绘制方法适配到我们系统的绘制接口上,这样就可以在不改变第三方库代码的情况下使用其功能。

3. 适配不同的数据格式

  • 在数据处理过程中,常常会遇到不同的数据格式需要进行转换的情况。适配器模式可以方便地实现数据格式的转换,使系统能够处理多种格式的数据。
  • 例如,一个数据分析系统需要处理来自不同数据源的数据,有的数据源提供的是 CSV 格式的数据,有的则是 JSON 格式的数据。可以为每种数据格式创建一个适配器,将不同格式的数据转换为系统内部统一的数据格式进行处理。

4. 适配不同的平台或环境

  • 当软件需要在不同的平台或环境下运行时,可能会遇到接口不一致的问题。适配器模式可以解决这个问题,使软件能够在不同的平台上正常工作。
  • 比如,一个跨平台的应用程序需要在 Windows 和 Linux 系统上运行,但这两个系统的文件操作接口有所不同。可以创建一个适配器,将应用程序对文件操作的请求适配到不同系统的文件操作接口上,实现跨平台的文件操作。

  1. 结构型设计模式的核心目的是通过改变对象的组合方式,来实现系统的功能扩展和优化。它可以帮助开发者处理类和对象之间的关系,解决接口不兼容、代码复用、系统结构优化等问题,使系统的设计更加灵活、可维护和可扩展。常见结构型设计模式有:适配器模式、桥接模式、装饰器模式、外观模式、享元模式、代理模式等。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值