Bootstrap

适配器模式

现实世界的例子

        假设你的存储卡里有一些照片,你需要将它们传输到电脑上。为了传输这些照片,你需要一种与电脑端口兼容的适配器,以便将存储卡连接到电脑上。在这种情况下,读卡器就是一个适配器。另一个例子是著名的电源适配器;一个三脚插头无法连接到两孔的插座上,它需要使用一个电源适配器来使其与两孔插座兼容。还有一个例子是翻译人员将一个人说的话翻译成另一种语言给另一个人听。

用通俗易懂的话来说:

        适配器模式允许你将一个不兼容的对象包装在一个适配器中,使其与另一个类兼容。

维基百科上说:

        在软件工程中,适配器模式是一种软件设计模式,它允许一个现有类的接口被用作另一个接口。它通常用于使现有类与其他类一起工作,而无需修改它们的源代码。

程序示例

1. 问题背景

        我们有一个游戏,游戏里有猎人(Hunter)和狮子(Lion),猎人可以“捕猎”狮子。假设在游戏的设计中,我们需要一种“狮子”接口,这样就可以让猎人捕猎不同类型的狮子,比如非洲狮(AfricanLion)和亚洲狮(AsianLion)。

        为了做到这一点,我们先定义了一个Lion接口,任何“狮子”的类型都需要实现它。

2. Lion 接口

        这个接口Lion有一个函数roar(),代表狮子的吼叫行为。我们可以理解为:任何“狮子”类型都应该具备“吼叫”的行为。

// Lion接口
class Lion {
public:
    virtual void roar() = 0; // 定义虚函数 roar
    virtual ~Lion() = default; // 默认析构函数
};

3. 两种具体的“狮子”类型

        为了实现不同的狮子,我们定义了非洲狮(AfricanLion)和亚洲狮(AsianLion),它们都实现了Lion接口,并重写了roar()函数。

class AfricanLion : public Lion {
public:
    void roar() override {
        std::cout << "非洲狮吼叫!\n";
    }
};

class AsianLion : public Lion {
public:
    void roar() override {
        std::cout << "亚洲狮吼叫!\n";
    }
};

4. Hunter 类捕猎狮子

        猎人Hunter类里有一个hunt()函数,接收Lion接口类型的对象。猎人通过调用狮子的roar()方法,来实现“捕猎”行为。

class Hunter {
public:
    void hunt(std::shared_ptr<Lion> lion) {
        lion->roar(); // 调用 Lion 的 roar 方法
    }
};

        这样,猎人就可以捕猎任何一种实现了Lion接口的对象(非洲狮、亚洲狮等)。

5. 新增的“野狗”问题

        现在,我们需要在游戏中增加一种新的动物野狗(WildDog)。但是问题来了:野狗的接口和狮子的接口不兼容,因为野狗只有bark()方法,没有roar()方法。

class WildDog {
public:
    void bark() {
        std::cout << "野狗叫!\n";
    }
};

6. 使用适配器模式解决问题

        为了让猎人捕猎野狗,我们需要一个中间类,把“野狗”适配成“狮子”。这时候,我们可以使用适配器模式来解决。

        WildDogAdapter类就是我们的适配器。它实现了Lion接口,并包含一个WildDog对象。在适配器类里,我们实现了roar()方法,但在内部调用了WildDog的bark()方法来模拟吼叫。

class WildDogAdapter : public Lion {
private:
    std::shared_ptr<WildDog> dog;

public:
    WildDogAdapter(std::shared_ptr<WildDog> dog) : dog(dog) {}

    void roar() override {
        dog->bark(); // 将 roar() 调用转为 bark()
    }
};

7. 使用适配器后的效果

        通过WildDogAdapter,现在我们可以让猎人捕猎野狗,因为WildDogAdapter已经符合Lion接口了,猎人可以像对待“狮子”那样对待“野狗”:

int main() {
    std::shared_ptr<Lion> africanLion = std::make_shared<AfricanLion>();
    std::shared_ptr<Lion> asianLion = std::make_shared<AsianLion>();
    std::shared_ptr<WildDog> wildDog = std::make_shared<WildDog>();

    Hunter hunter;
    hunter.hunt(africanLion);  // 输出:非洲狮吼叫!
    hunter.hunt(asianLion);    // 输出:亚洲狮吼叫!

    // 使用 WildDogAdapter 适配器来让猎人捕猎野狗
    std::shared_ptr<Lion> wildDogAdapter = std::make_shared<WildDogAdapter>(wildDog);
    hunter.hunt(wildDogAdapter);  // 输出:野狗叫!
    
    return 0;
}

总结

  • Lion 接口:定义了狮子的标准接口,即roar()。
  • AfricanLion 和 AsianLion:实现了狮子的接口,可以被猎人直接捕猎。
  • WildDog:接口不兼容狮子,无法被猎人捕猎。
  • WildDogAdapter:将WildDog适配成符合Lion接口的形式,从而让猎人可以捕猎“野狗”。

        适配器模式主要用在接口不兼容的情况下,通过适配器使得两个不兼容的类可以协同工作。具体来说,它通常在以下几种场景下使用:

  • 系统需要整合第三方组件或库:

        当现有系统需要使用第三方库或组件,但其接口与当前系统不兼容时,使用适配器模式将第三方类包装成符合系统标准接口的形式,这样可以无缝使用外部库功能而无需修改系统代码。例如,将一个特定数据库驱动适配为系统的通用数据库接口。

  • 需要复用现有的类,而这些类的接口与目标接口不兼容:

        在新系统中想重用旧系统中的类,但旧类的接口不同于新系统中的接口,可以通过适配器进行包装,避免对旧类的直接修改。例如,将一个旧的支付接口适配到新支付系统中,使旧代码兼容新系统。

  • 统一接口格式,便于扩展:

        当系统中有多个类,它们的接口不一致,但实际功能类似时,可以通过适配器模式使它们实现相同的接口,便于后续扩展和维护。例如,不同厂商的图像处理工具有不同的接口,可以通过适配器模式将它们统一为相同的图像处理接口。

  • 转换接口以实现兼容性:

        当两个不兼容的接口需要协同工作时,适配器可以在其中扮演“翻译器”的角色。例如,在游戏开发中,一个新加入的对象(如不同种类的动物)需要适应现有的游戏规则接口,可以使用适配器模式使它符合规则。

        适配器模式的核心在于实现接口转换,使不兼容的类可以兼容工作,特别适用于集成外部系统、复用不兼容的现有类、以及统一接口标准的场景。

;