ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

C++ 3个常用API包装器模式:代理模式、适配器模式、外观模式

2022-06-28 01:02:54  阅读:248  来源: 互联网

标签:对象 适配器 代理 接口 API 模式


目录

API包装器模式

通常,需要编写基于另一组类的包装器接口,用一个新的、更简洁的API,来隐藏所有底层遗留代码;或者,已经编写了C++ API,但后来需要给特定客户提供纯C接口,但又不想改变原来的代码封装;或者,你的API用的了一个第三方依赖库,你想让客户直接使用此库,但不想将此库直接暴露给客户。

包装器API的潜在副作用是影响性能,因为会增加一级额外的函数调用,以及存储保证层次状态带来的开销。但好处也是明显的,可以创建质量更高、更适配用户需求的API接口。

C++中,有3个常用的API包装器模式:代理模式,适配器模式,外观模式。它们都属于结构型模式,按包装器层和原始接口的差异递增。

代理模式

在GoF中,代理模式的意图定义为:为其他对象提供一种代理以控制对这个对象的访问。

代理模式提供一对一的转发接口,代理类和原始类应该具有相同接口。实现方式,可以是代理类存储原始类对象(即被代理类,也叫真实对象)的副本,或者指向原始类对象的指针(是不是很像Pimpl惯用法?),然后代理类的方法将重定向到原始类对象中的同名方法。

缺点:1)将原始类接口暴露给用户;2)在改变原始类接口时,需要维护代理接口的完整性。

代理模式 vs Impl惯用法

代理类形式上有点像Pimpl惯用法(见C++ Pimpl惯用法(桥接模式特例)),区别在哪?

  • Pimpl惯用法属于桥接模式特例,主要目的是通过接口类实现对用户屏蔽实现细节,重在隐藏实现细节。私有实现类(Impl class)和接口类的API接口没必要保持一致,而且私有实现类往往与接口类是同一个实现者。

  • 代理模式主要目的是为用户提供控制对原始类对象的访问,要求接口必须保持与原始类接口一致,重在控制,即不能改变功能。被代理类经常是第三方库,或者已经设计好的部分。

代理模式的简单实现

代理类

class Proxy
{
public:
    Proxy() : original_(new Original())
    {}
    ~Proxy()
    {
        delete original_;
    }

private:
    Proxy(const Proxy&);
    const Proxy &operator=(const Proxy&);

    Original *original_;
};

将代理类设计为禁止copy,因为原始类不仅仅是一个类,背后往往设计到其他资源,仅仅拷贝对象并没有实际意义;当然,如果有copy对象需求,可以自行实现copy函数。

另一种方案,是在此方案基础上增加代理和原始API共享的virtual接口,目的在于通过C++语法来保持2个API同步。这样做前提是你能修改原始API。

// 通过公有接口IOriginal, 从语法层面确保代理类和被代理类接口一致

class IOriginal // 原始类接口
{
public:
    virtual bool DoSomething(int value) = 0;
};

class Original : public IOriginal // 原始类
{
public:
    bool DoSomething(int value);
};

class Proxy : public IOriginal // 代理类
{
public:
    Proxy() : original_(new Original())
    {}
    ~Proxy()
    {
        delete original_;
    }

    bool DoSomething(int value)
    {
        return original_->DoSomething(value);
    }

private:
    Proxy(const Proxy &);
    const Proxy &operator=(const Proxy&);
    
    Original *original_;
};

代理模式应用场景

1)实现原始对象的惰性实例化。直到特定方法被调用时,即对象真正被需要时,Original对象才真正实例化。

2)实现对Original对象的访问控制。如要在Proxy和Original对象之间插入权限层,确保当用户获得适当的授权后,只能调用Original对象上的特定方法。

3)支持调试或“演习”模式。支持在Proxy方法中插入调试语句,记录所有对Original对象的调用,或者使用一个标志以“演习”(dry run)模式调用Proxy,以禁止调用特定的Original方法;例如,禁止将对象状态写入磁盘。

4)保证Original类线程安全。通过给非线程安全地方法添加互斥锁实现线程安全。虽然不是确保线程安全的最佳实践,但如果不能修改Original,却也是一个权宜之计。比如为glibc库函数加锁。

5)支持资源共享。当多个Proxy对象共享相同的Original基础类。例如,可用于实现引用计数或写时复制(copy-on-write)语义。这种用法实际上是享元模式(Flyweight)。

6)应对Original类将来被修改的情况。如果预期依赖库可能会改变,可以为其API创建一个代理包装器模拟当前的行为。当库改变时,通过代理对象预留老的接口,改变代理类的底层实现,就可以使用新的库方法。准确来说,这是适配器对象,而非代理对象。


适配器模式

适配器模式意图:将一个类的接口转换成客户希望的另外一个接口。

也就是说,适配器提供接口转换功能。真实对象接口与客户希望的接口并不兼容,可能由于参数顺序或类型不同,使用习惯不同,命名约定不同等等原因,导致无法直接使用。此时,可以用适配器模式对真实对象接口进行转换。

适配器模式 vs 代理模式

两种相同点在于,可用来对真实对象进行API包装,被包装的对象经常是第三方依赖库,无法改变;另外,两者都不会改变真实对象的基本功能。

不同点:

  • 代理模式 要求不改变真实对象的接口,代理模式重在提供控制对象的访问。

  • 适配器模式 通常需要改变真实对象的接口(名字,参数个数、类型、顺序,返回值类型),适配器模式重在适配接口。

适配器模式简单实现

例如,现有真实对象类Rectangle 提供接口setDimension,是以圆心、半径、矩形宽和高方的式定义矩形,而客户需要通过矩形左下角、右上角坐标来定义矩形。可以通过适配器类RectangleAdapter,来对接口进行转换。

class RectangleAdapter
{
public:
    RectangleAdapter() :
    rect_(new Rectangle())
    {}

    ~RectangleAdapter()
    {
        delete rect_;
    }

    void Set(float x1, float y1, float x2, float y2)
    {
        float w = x2 - x1;
        float h = y2 - y1;
        float cx = w / 2.0f + x1;
        float cy = h / 2.0f + y1;
        rect_->setDimension(cx, cy, w, h);
    }

private:
    // 禁止copy
    RectangleAdapter(const RectangleAdapter&);
    const RectangleAdapter& operator=(const RectangleAdapter&);
    
    Rectangle *rect_;
};

适配器可以通过“组合”或“继承”来实现。前者称为对象适配器,后者称为类适配器。上面示例,显然是对象适配器。

适配器模式优点

1)强制API始终保持一致性。使用适配器模式能整合接口风格不同的类,为它们提供一致的接口供客户使用。

2)包装API的依赖库。可以不暴露依赖库及其接口给用户。

3)转换数据类型。例如,将极坐标转换为直角坐标。

4)为API暴露一个不同的调用约定。例如,为纯C API提供面向对象版本。另一个常用的场景,就是将系统调用封装成RAII管理方式,同时伴随着C++接口包装C接口。


外观模式

外观模式对意图:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

一个子系统可能包含多个对象,它们的API可能各不相同,如果让用户针对每个对象都定制一个调用,可能会有多种风格调用,而且相互之间很难转换。这将势必导致用户不得不了解个对象接口细节。而外观模式,为这个子系统内的所有API提供一个统一的接口,简化其使用,用户只需要对接外观模式即可。

一个典型的外观模式,就是Linux的系统调用open/close/read/write/fcntl等,用户只需要针对文件描述符调用这些函数即可,而不必关系操作的具体是什么设备。

外观模式 vs 适配器模式

外观模式与适配器模式都是改变被包装类的接口,它们有什么区别?

外观模式简化了类的结构,而适配器模式仍保持类的结构(并未修改类的结构关系)。外观模式通常是为一组对象提供统一接口,目的在于提供统一风格的接口;而一个适配器模式,往往针对一个真实对象(类)提供适配接口。

外观模式的简单实现

假设你在度假并入住了一家酒店。你计划先用晚餐,然后去看演出。如果不用外观模式,你需要先给餐厅打电话预订晚餐,接着打电话给剧院预订座位,可能还需要叫出租车来接你。在C++中,可将这3件事表示为3个独立的对象,并逐一处理每个对象。

class Taxi
{
public:
    bool BookTaxi(int npeople, time_t pickup_time); // 预订出租车
};

class Restaurant
{
public:
    bool ReserveTable(int npeople, time_t arrival_time); // 订晚餐
};

class Theater
{
public:
    time_t GetShowTime();
    bool ReserveSeats(int npeople, int tier); // 预订座位
};

假设你入住的是一家高档酒店,酒店的礼宾部能帮你完成所有事情。礼宾部首先查演出时间,然后根据掌握的当地情况,计算出合适的晚餐时间,并在最佳时间为你预订出租车。转换成C++术语,你只需要对接礼宾部对象即可,而且该对象的接口比使用上面3个对象接口更简单。

class ConciergeFacde
{
public:
    enum ERestaurant {
        RESTAURANT_YES,
        RESTAURANT_NO
    };
    enum ETaxi {
        TAXI_YES,
        TAXI_NO
    };

    // 现在, 你只需要关心这一个对象的接口即可
    time_t BookShow(int npeople, ERestaurant addRestaurant, ETaxi addTaxi);
};

外观模式优点

1)隐藏遗留代码。原有系统可能较为陈旧、脆弱,不提供一致的对象模型。而外观模式能有效基于原有代码创建一组设计良好的API,用户只需要使用新API即可。

2)创建便捷API。例如,OpenGL的GL库提供底层的基础例程,功能强大,但使用繁琐;GLU库基于GL库,提供高层次易于使用的接口。

3)支持简化功能或替代功能的API。抽象出对底层的子系统的访问后,就能替换某个子系统,且捕获影响客户代码。


参考

[1]Martin Reddy, 刘晓娜, 臧秀涛,等. C++ API设计[M]. 人民邮电出版社, 2013.

标签:对象,适配器,代理,接口,API,模式
来源: https://www.cnblogs.com/fortunely/p/16418080.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有