C++ 设计模式 —— 适配器模式

235 阅读9分钟

C++ Adapter Pattern 理论与实践

0. 参考链接

本文参考了以下链接的内容:

1. 文章简介

这篇文章,我会根据这些参考链接的内容,整理成自己的语言,并加入自己的思考后,编写的一些代码和分析。将从下面几个方面介绍 C++ 适配器模式:

  • 适配器模式的定义和作用
  • 适配器的实现原理
  • 适配器模式在 C++ 中的应用实例

1.1 适配器模式的定义

  • 适配器模式(Adapter Pattern)是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。
  • 适配器模式把一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  • 适配器模式分为类适配器模式和对象适配器模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件的接口,所以应用相对较少。

1.2 适配器模式适合的业务场景

  • 系统需要使用现有的类,而此类的接口不符合现有的系统需求,需要改进,但你不能修改现有的类的情况
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
  • 两个类所做的事情相同或相似,但是接口不一致,需要通过适配器来适配
  • 可以通过封装适配器类来模拟一个类的行为

2. 适配器模式的原理

2.1 角色定义

  • 适配器模式包含如下角色:
    • Target:目标抽象类
    • Adapter:适配器类
    • Adaptee:适配者类
    • Client:客户端类

2.1.1 Target(目标接口)

  • 定义客户端使用的接口
  • 包含一个或多个方法,用于与客户端交互

2.1.2 Adaptee(被适配者接口)

  • 定义已有的接口,但不符合客户端的期望
  • 包含一些有用的行为,但其接口与客户端不兼容

2.1.3 Adapter(适配器)

  • 实现目标接口,并包含对被适配者接口的引用
  • 包含必要的方法调用以调用被适配者的方法,并在其基础上添加额外的功能
  • 拥有同时与Client和Adaptee交互的能力的类
  • 通过接受Client的接口调用并将其转换为合适的方法调用,以调用Adaptee类的方法

2.1.4 Client(客户端)

  • 真实使用适配器进行业务代码编写的类
  • 负责组装不同适配器的业务,以达到满足目标接口要求的目的

2.2 示例代码实现

// Target interface
class Target {
public:
    virtual ~Target() = default;
    virtual std::string Request() const {
        return "Target: The default target's behavior.";
    }
};

// Adaptee interface (incompatible with the client)
class Adaptee {
public:
    std::string SpecificRequest() const {
        return ".eetpadA eht fo roivaheb laicepS";
    }
};

// Adapter class (multiple inheritance)
class Adapter : public Target, public Adaptee {
public:
    std::string Request() const override {
        std::string to_reverse = SpecificRequest();
        std::reverse(to_reverse.begin(), to_reverse.end());
        return "Adapter: (TRANSLATED) " + to_reverse;
    }
};

2.3 方钉打孔问题

2.3.1 伪代码描述

// 假设你有两个接口相互兼容的类:圆孔(Round­Hole)和圆钉(Round­Peg)。
class RoundHole is
    constructor RoundHole(radius) { …… }

    method getRadius() is
        // 返回孔的半径。

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { …… }

    method getRadius() is
        // 返回钉子的半径。


// 但还有一个不兼容的类:方钉(Square­Peg)。
class SquarePeg is
    constructor SquarePeg(width) { …… }

    method getWidth() is
        // 返回方钉的宽度。


// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
    // 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // 适配器会假扮为一个圆钉,其半径刚好能与适配器实际封装的方钉搭配
        // 起来。
        return peg.getWidth() * Math.sqrt(2) / 2


// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

2.3.2 C++ 代码实现

class RoundPeg
{
public:
    /// Constructor for a RoundPeg with a given radius
    RoundPeg(double radius) : radius_(radius) {}

    /// Get the radius of the RoundPeg
    virtual double getRadius() const
    {
        return radius_;
    }

private:
    /// Private data member for the radius
    double radius_;
};

class RoundHole
{
public:
    /// Constructor for a RoundHole.
    /// @param radius the radius of the RoundHole.
    RoundHole(double radius) : radius_(radius) {}

    /// Get the radius of the RoundHole
    double getRadius() const
    {
        return radius_;
    }

    /// Checks if the RoundHole can fit the given RoundPeg.
    /// @param peg the RoundPeg to check.
    bool fits(const RoundPeg& peg) const
    {
        return radius_ >= peg.getRadius();
    }

private:
    /// The radius of the RoundHole.
    double radius_;
};

class SquarePeg
{
public:
    // Constructor for a SquarePeg object.
    // Takes in a double value for the width.
    SquarePeg(double width) : width_(width) {}

    // Getter function for the width of the SquarePeg.
    double getWidth() const
    {
        return width_;
    }

private:
    // Private data members.
    double width_;
};

class SquarePegAdapter : public RoundPeg
{
public:
    /// Constructor from a SquarePeg.
    SquarePegAdapter(const SquarePeg& peg) : RoundPeg(peg.getWidth()* sqrt(2) / 2), peg_(peg) {}

    /// Get the radius of the RoundPeg.
    double getRadius() const override
    {
        return RoundPeg::getRadius();
    }

    /// Get the SquarePeg.
    const SquarePeg& getPeg() const
    {
        return peg_;
    }

private:
    /// The SquarePeg.
    const SquarePeg& peg_;
};

void RoundAdapterExample()
{
    // Create a RoundHole with a radius of 5.
    RoundHole hole(5);
    // Create a RoundPeg with a radius of 5.
    RoundPeg rpeg(5);
    // Print whether the RoundPeg's radius is inside the RoundHole's radius.
    std::cout << "Is " << rpeg.getRadius() << " inside " << hole.getRadius() << "? " << hole.fits(rpeg) << std::endl;

    // Create a SquarePeg with a width of 5.
    SquarePeg small_sqpeg(5);
    // Create a SquarePeg with a width of 10.
    SquarePeg large_sqpeg(10);
    // Print whether the SquarePeg's width is inside the RoundHole's radius.
    std::cout << "Is " << small_sqpeg.getWidth() << " inside " << hole.getRadius() << "? " /* << hole.fits(small_sqpeg)*/ << std::endl;

    // Create a SquarePegAdapter from the small_sqpeg.
    SquarePegAdapter small_sqpeg_adapter(small_sqpeg);
    // Create a SquarePegAdapter from the large_sqpeg.
    SquarePegAdapter large_sqpeg_adapter(large_sqpeg);
    // Print whether the SquarePegAdapter's radius is inside the RoundHole's radius.
    std::cout << "Is " << small_sqpeg_adapter.getRadius() << " inside " << hole.getRadius() << "? " << hole.fits(small_sqpeg_adapter) << std::endl;
    std::cout << "Is " << large_sqpeg_adapter.getRadius() << " inside " << hole.getRadius() << "? " << hole.fits(large_sqpeg_adapter) << std::endl;
}

2.4 数据转换案例

首先,我们需要创建一个表示原始数据格式的类:

class OriginalData
{
public:
    std::string name;
    int age;

    OriginalData(const std::string &name, int age) : name(name), age(age) {}
};

接下来,我们需要创建一个表示目标数据格式的类:

class TargetData
{
public:
    std::string name;
    std::string email;

    TargetData(const std::string &name, const std::string &email)
        : name(name), email(email) {}
};

然后,我们需要创建一个适配器类,将OriginalData转换为TargetData:

class OriginalToTargetAdapter
{
public:
    TargetData *convert(const OriginalData &originalData)
    {
        TargetData *targetData = new TargetData(originalData.name, "");
        return targetData;
    }
};

最后,我们可以使用适配器将OriginalData转换为TargetData:

void OriginalToTargetTest()
{
    OriginalData originalData("John Doe", 30);
    OriginalToTargetAdapter adapter;
    TargetData *targetData = adapter.convert(originalData);

    std::cout << "Name: " << targetData->name << std::endl;
    std::cout << "Email: " << targetData->email << std::endl;
}

3. 适配器模式的优点和缺点

3.1 优点

  • 客户端代码不需要了解适配器内部的实现细节,只需了解目标接口即可。
  • 提高了代码的复用性,因为可以在多个类之间共享相同的适配器代码。
  • 单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

3.2 缺点

  • 适配器模式增加了系统的复杂度,因为需要新建一个对象。
  • 适配器模式可能会导致类的个数膨胀。
  • 适配器模式有时候为了兼容会导致代码的可读性变差。

4. 代码分析

4.1 2.1 代码分析

这段代码展示了一个适配器模式的示例。适配器模式用于在不修改现有类的情况下,通过引入新的接口来使其与其他不兼容的接口协同工作。

以下是对代码的分析和解释:

class Target {
public:
    virtual ~Target() = default;
    virtual std::string Request() const {
        return "Target: The default target's behavior.";
    }
};

Target 是一个抽象基类,它定义了一个纯虚函数 Request(),该函数返回一个字符串表示默认目标的行为。

// Adaptee interface (incompatible with the client)
class Adaptee {
public:
    std::string SpecificRequest() const {
        return ".eetpadA eht fo roivaheb laicepS";
    }
};

Adaptee 是一个与客户端不兼容的接口,它有一个成员函数 SpecificRequest(),返回一个特定的字符串。

// Adapter class (multiple inheritance)
class Adapter : public Target, public Adaptee {
public:
    std::string Request() const override {
        std::string to_reverse = SpecificRequest();
        std::reverse(to_reverse.begin(), to_reverse.end());
        return "Adapter: (TRANSLATED) " + to_reverse;
    }
};

Adapter 类继承了 TargetAdaptee 两个接口,并重写了 Request() 函数。在 Request() 函数中,首先调用了 SpecificRequest() 函数获取一个字符串,然后对该字符串进行反转操作,最后将反转后的字符串与前缀 "Adapter: (TRANSLATED) " 拼接起来作为结果返回。

适配器模式的作用是将原本不兼容的接口转换为兼容的接口,使得原本需要使用复杂方式才能访问的数据或功能能够以简单的方式被访问。在这个例子中,Adapter 类充当了适配器的角色,使得原本需要直接使用 Adaptee 接口才能访问的数据可以通过 Target 接口间接访问,并且还添加了一些额外的处理逻辑。

4.2 2.2 方钉代码分析

这段代码展示了适配器模式在几个方面的应用。适配器模式是一种结构型设计模式,它允许你使用现有类的接口来与其他不兼容的类一起工作。

  1. SquarePegAdapter: 这是一个适配器类,它从RoundPeg类继承,并实现了RoundPeg类的接口。这个类的主要目的是适配SquarePegRoundHole之间的不兼容性。它通过添加一个SquarePeg成员变量来实现这一点,并在需要时提供对SquarePeg的访问。

  2. 构造函数: SquarePegAdapter的构造函数接受一个SquarePeg对象作为参数,然后计算其对应的RoundPeg的半径(宽度乘以根号2除以2)。同时,它还保存了传入的SquarePeg对象的引用,以便在需要时可以访问它。

  3. getRadius()和getPeg()方法: getRadius()方法返回RoundPeg的半径,而getPeg()方法返回SquarePeg对象的引用。这两个方法都覆盖了RoundPeg类的相应方法。

  4. fits()方法: SquarePegAdapter类还重写了RoundHole类的fits()方法。这个方法检查RoundHole的半径是否大于或等于RoundPeg的半径,如果是,则返回true,否则返回false

整体上,我写的这段代码展示了如何使用适配器模式来解决不兼容接口之间的问题。通过创建一个新的接口,可以将旧的接口转换为新的接口,从而使两个不兼容的类能够一起工作。

个人格言

追寻与内心共鸣的生活,未来会逐渐揭晓答案。

Pursue the life that resonates with your heart, and the future will gradually reveal the answer.