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

目录

  • API包装器模式
    • 代理模式
      • 代理模式 vs Impl惯用法
      • 代理模式的简单实现
      • 代理模式应用场景
    • 适配器模式
      • 适配器模式 vs 代理模式
      • 适配器模式简单实现
      • 适配器模式优点
    • 外观模式
      • 外观模式 vs 适配器模式
      • 外观模式的简单实现
      • 外观模式优点
    • 参考

 

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.

本文来自博客园,作者:明明1109,转载请注明原文链接:https://www.cnblogs.com/fortunely/p/16418080.html

 

 

 

 https://www.cnblogs.com/fortunely/p/16418080.html

 

posted @   imxiangzi  阅读(115)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示