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

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

适配者模式(Adapter Pattern)是一种结构型设计模式,主要用于解决接口不兼容的问题。其核心思想是通过创建一个适配器类,将一个类的接口转换成客户端所期待的另一种接口。

1. 主要组成成分

  • 目标抽象类(Target):这是客户端所期望的接口,定义了客户端使用的方法。
  • 源类(Adaptee):这是需要被适配的类,拥有某些功能,但其接口与目标接口不兼容。
  • 适配器类(Adapter):适配器类实现目标接口,并持有一个源类的实例。在适配器中,适配器会调用源类的方法,以满足目标接口的要求。
  • 客户端(Client):客户端通过目标接口与适配器进行交互,而不是直接与源类交互。

2. 逐步构建适配者模式

以日志系统为例,逐步构建适配者模式。

2.1 目标抽象类定义

目标抽象类LogToDatabase,包含与日志数据库操作相关的方法:初始化数据库、写入日志、读取日志和关闭数据库。

//目标接口
class LogToDatabase
{
public:
    virtual void initdb() = 0; //不一定非是纯虚函数
    virtual void writetodb(const char* pcontent) = 0;
    virtual void readfromdb() = 0;
    virtual void closedb() = 0;

    virtual ~LogToDatabase() {} //做父类时析构函数应该为虚函数
};
2.2 源类实现

定义源类 LogToFile,实现日志文件操作的方法,这些方法与目标接口不兼容,需要通过适配器转换。

//源类
class LogToFile
{
public:
    void initfile()
    {
        //做日志文件初始化工作,比如打开文件等等
        //......
    }
    void writetofile(const char* pcontent)
    {
        //将日志内容写入文件
        //......
    }
    void readfromfile()
    {
        //从日志中读取一些信息
        //......
    }
    void closefile()
    {
        //关闭日志文件
        //......
    }
    //......可能还有很多其他成员函数,略
};
2.3 适配器类实现

实现适配器类 LogAdapter,它继承自目标接口 LogToDatabase。构造函数接受 LogToFile 对象的指针,实现目标接口的方法时调用源类的方法。

//适配器类
class LogAdapter :public LogToDatabase
{
public:
    //构造函数
    LogAdapter(LogToFile* pfile) //形参是老接口所属类的指针
    {
        m_pfile = pfile;
    }
    virtual void initdb()
    {
        cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
        //这其中也可以加任何的其他代码......
        m_pfile->initfile();
    }
    virtual void writetodb(const char* pcontent)
    {
        cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
        m_pfile->writetofile(pcontent);
    }
    virtual void readfromdb()
    {
        cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromdb()" << endl;
        m_pfile->readfromfile();
    }
    virtual void closedb()
    {
        cout << "在LogAdapter::closedb()中适配LogToFile::closedb()" << endl;
        m_pfile->closefile();
    }
private:
    LogToFile* m_pfile;
};
2.4 客户端

main 函数中,创建 LogToFileLogAdapter 实例,并通过适配器调用目标接口的方法。

//客户端使用
int main()
{
    LogToFile* plog = new LogToFile();
    LogToDatabase* plogdb = new LogAdapter(plog);
    plogdb->initdb();
    plogdb->writetodb("向数据库中写入一条日志,实际是向日志文件中写入一条日志");
    plogdb->readfromdb();
    plogdb->closedb();
    delete plogdb;
    delete plog;

    return 0;
}

3. 适配者模式 UML 图

适配者模式 UML 图

适配者模式 UML 图解析
  • Target (目标抽象类): 此类定义了客户端期望使用的接口,如 initdbwritetodbreadfromdbclosedb 等。这些接口代表了调用者希望利用的未来接口,并将由客户端直接调用。这里指LogToDatabase 类。
  • Adaptee (源类): 这个类扮演被适配的角色,拥有一套已经存在的接口,这些接口与目标接口不兼容需要被适配。适配涉及将调用目标接口的行为转换为对这些已存在接口的调用。在适配者模式中,适配者类可以不止一个,示例中的 LogToFile 类就是这样一个适配者。
  • Adapter (适配器类): 适配器类是适配者模式的核心,它连接 Target 和 Adaptee 角色。此类通过包装一个或多个 Adaptee 对象,使得 Adaptee 的接口看起来像是 Target 的接口。适配器类负责将 Target 接口的调用转换为 Adaptee 接口的调用,实现接口的兼容。在给出的例子中,这个角色由 LogAdapter 类实现。

5. 类适配者

除了上面介绍的对象适配器,还有一种实现方式叫做类适配器。类适配器使用多重继承来实现适配,这在C++中是可行的,但在Java等不支持多重继承的语言中就不能使用。以下是类适配器的实现示例:

类适配器与对象适配器的主要区别在于:类适配器通过继承来实现适配,而对象适配器通过组合来实现适配。类适配器可以重写Adaptee的行为,但也会增加耦合度。

//类适配器
class LogAdapter :public LogToDatabase, private LogToFile
{
public:
    virtual void initdb()
    {
        cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
        //这其中也可以加任何的其他代码......
        initfile();
    }
    virtual void writetodb(const char* pcontent)
    {
        cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
        writetofile(pcontent);
    }
    virtual void readfromdb()
    {
        cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromdb()" << endl;
        readfromfile();
    }
    virtual void closedb()
    {
        cout << "在LogAdapter::closedb()中适配LogToFile::closedb()" << endl;
        closefile();
    }
};
// 客户端
int main()
{
    LogToDatabase* plogdb = new LogAdapter();
    plogdb->initdb();
    plogdb->writetodb("向数据库中写入一条日志,实际是向日志文件中写入一条日志");
    plogdb->readfromdb();
    plogdb->closedb();
    delete plogdb;

    return 0;
}

6. 适配者模式的优点

  • 灵活性:可以通过添加新的适配器来支持新接口,而无需修改现有代码。
  • 复用性:可复用现有的类,减少了重复代码。
  • 解耦:客户端与源类之间的关系通过适配器解耦,降低了系统的复杂性。

7. 适配者模式的缺点

  • 增加复杂性:引入适配器类可能使系统结构变得更加复杂。
  • 性能开销:适配器增加了一层间接调用,可能影响性能,但在大多数情况下影响微乎其微。

8. 适配者模式适用场景

  • 已存在的类的接口不符合系统的需求:当系统需要使用现有的类,但这些类的接口不符合系统的需求时,可以使用适配者模式来使现有的类与系统接口兼容。
  • 需要创建一个可复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作:适配器模式可以提供一个中间层,通过包装一个不兼容的对象,将其接口转换成目标接口,从而使其能与多种不同的对象协同工作。
  • 在需要使用几个现有的子类,但是子类的接口不一致时:可以使用适配者模式来适配这些接口。通过定义一个统一的接口,并在适配器中将调用分派给相应的子类接口,可以使得原本由于接口不兼容而不能一起工作的类可以一起工作。
  • 系统需要使用现有的类,而类的接口不符合系统的需求:例如,系统数据的输入需要特定的格式,而现有的库提供的数据格式与之不符。适配器可以在这两者之间进行转换。
  • 整合多个库或框架时:当使用多个库或框架构建应用程序时,经常会遇到因为接口不兼容而无法一起工作的情况。使用适配器模式可以解决这些库或框架间的接口不兼容问题,使它们可以一起工作。
  • 替换系统中的旧组件时:在软件维护或升级过程中,旧的组件可能需要被更现代或功能更强大的组件替换。如果新组件的接口与系统现有的接口不匹配,适配器模式可以用来适配这些接口,从而允许系统平滑过渡到新的组件,而不需要重写大量的代码。

总结

适配者模式是一种强大而灵活的设计模式,它允许不兼容的接口能够协同工作。通过创建适配器,我们可以复用现有的类,而无需修改其代码。这种模式特别适用于系统集成、旧系统改造或者与第三方库协作的场景。

适配者模式的核心在于:

  1. 识别目标接口和源类之间的差异。
  2. 设计适配器类来桥接这些差异。
  3. 在适配器中实现接口转换逻辑。

在使用适配者模式时,需要注意以下几点:

  1. 确保适配器只处理接口转换,不要在其中添加额外的业务逻辑。
  2. 考虑使用对象适配器还是类适配器,根据实际需求和语言特性来选择。
  3. 当需要适配的类较多时,可以考虑使用工厂模式来创建适配器。

虽然适配者模式可能会增加一些复杂性,但它提供的灵活性和可维护性通常会超过这些缺点。在使用时,应该权衡其利弊,选择最适合特定场景的解决方案。

完整代码

#include <iostream>
#include <vector>
#include <string>
#include <fstream>

using namespace std;

//日志文件操作相关类
class LogToFile
{
public:
    void initfile()
    {
        //做日志文件初始化工作,比如打开文件等等
        //......
    }
    void writetofile(const char* pcontent)
    {
        //将日志内容写入文件
        //......
    }
    void readfromfile()
    {
        //从日志中读取一些信息
        //......
    }
    void closefile()
    {
        //关闭日志文件
        //......
    }
    //......可能还有很多其他成员函数,略
};

class LogToDatabase
{
public:
    virtual void initdb() = 0; //不一定非是纯虚函数
    virtual void writetodb(const char* pcontent) = 0;
    virtual void readfromdb() = 0;
    virtual void closedb() = 0;

    virtual ~LogToDatabase() {} //做父类时析构函数应该为虚函数
};

/*//适配器类
class LogAdapter :public LogToDatabase
{
public:
    //构造函数
    LogAdapter(LogToFile* pfile) //形参是老接口所属类的指针
    {
        m_pfile = pfile;
    }
    virtual void initdb()
    {
        cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
        //这其中也可以加任何的其他代码......
        m_pfile->initfile();
    }
    virtual void writetodb(const char* pcontent)
    {
        cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
        m_pfile->writetofile(pcontent);
    }
    virtual void readfromdb()
    {
        cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromdb()" << endl;
        m_pfile->readfromfile();
    }
    virtual void closedb()
    {
        cout << "在LogAdapter::closedb()中适配LogToFile::closedb()" << endl;
        m_pfile->closefile();
    }
private:
    LogToFile* m_pfile;
}*/;

//类适配器
class LogAdapter :public LogToDatabase, private LogToFile
{
public:
    virtual void initdb()
    {
        cout << "在LogAdapter::initdb()中适配LogToFile::initfile()" << endl;
        //这其中也可以加任何的其他代码......
        initfile();
    }
    virtual void writetodb(const char* pcontent)
    {
        cout << "在LogAdapter::writetodb()中适配LogToFile::writetofile()" << endl;
        writetofile(pcontent);
    }
    virtual void readfromdb()
    {
        cout << "在LogAdapter::readfromdb()中适配LogToFile::readfromdb()" << endl;
        readfromfile();
    }
    virtual void closedb()
    {
        cout << "在LogAdapter::closedb()中适配LogToFile::closedb()" << endl;
        closefile();
    }
};

int main()
{
    //    LogToFile* plog = new LogToFile();
    //    LogToDatabase* plogdb = new LogAdapter(plog);
    //    plogdb->initdb();
    //    plogdb->writetodb("向数据库中写入一条日志,实际是向日志文件中写入一条日志");
    //    plogdb->readfromdb();
    //    plogdb->closedb();
    //    delete plogdb;
    //    delete plog;

    LogToDatabase* plogdb = new LogAdapter();
    plogdb->initdb();
    plogdb->writetodb("向数据库中写入一条日志,实际是向日志文件中写入一条日志");
    plogdb->readfromdb();
    plogdb->closedb();
    delete plogdb;

    return 0;
}
  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值