C++设计模式 - 代理模式

2022-12-01 16:06:17 浏览数 (1)

前言

曾经豪言壮志、目空一切,经过现实的软磨硬泡后,变得圆润光滑、随波逐流。是成熟了,还是堕落了。愿诸位阅尽繁华心弥坚,踏遍沧桑仍少年!

代理模式

❝代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。 ❞

在某些情况下,客户端代码不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。由代理对象向客户端提供引用原对象的接口,客户端通过调用代理对象访问原对象。

意义

首先要明白代理对象存在的必要性,如果不存在代理对象会有什么问题?

代理模式在Android中被用到很多。其中根据其目的和实现方式,主要可分为以下几种:

「远程代理(Remote Proxy)」 客户端代码与目标对象不在同一进程、地址空间或主机,客户端无法直接调用目标对象接口。此时可以通过调用代理对象来模拟调用目标对象接口。其中,代理对象与目标对象的通讯可能通过Binder、socket或其他通信方式,无论通过何种方式客户端都无需关心。

「虚拟代理(Virtual Proxy)」 对于一些占用系统资源较多或者加载时间较长的对象,可以先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。

「保护代理(Protect Proxy)」 给不同的用户提供不同的对象访问权限。

「缓冲代理(Cache Proxy)」 为某一个目标操作的结果提供临时存储空间,以使更多用户可以共享这些结果。

「智能引用代理(Smart Reference Proxy)」 当一个对象被引用时提供一些额外的操作,比如将对象被调用的次数记录下来等。

应用场景

在嵌入的分层思想中,某些靠近底层的进程享有硬件资源控制,更高层的用户进程则没有此权限。现存在两个进程,假设进程A拥有led资源控制权,进程B不可直接操控Led。如何实现进程B控制Led。

分析

上述场景通过进程间通信搭建A、B进程的通讯,然后通过进程A响应进程B的命令完成对应的操作即可实现。

但是作为追求完美的程序猿,这种方式是最完美的吗?

上述的实现方式,虽然能够满足需求,但是也会存在以下问题:

  • 这些命令由谁来定,进程的维护者是否能够看一眼就能明白每个命令的含义及用处?
  • 假设B进程发送了错误的命令,是否会引起不必要的错误。
  • 进程B维护者与进程A维护者,不仅要对接功能问题,还需调试进程间通信功能。

代理模式能够轻松的缓解上述的问题,方案如下:

  • 由进程A的维护者提供代理对象的库与头文件,进程B只需要调用头文件的接口即可。
  • 其中代理对象与真实对象的进程间通讯由A负责维护,B无需关心。

在开发工作中,相比于一个人维护,多个开发者交叉对接更不稳定。另外A可以通过增加或删除部分代理对象接口,控制B进程得到使用范围。

类图

享元模式.png

  • IledManager: 为代理对象与真实对象提供统一接口。
  • LedManager: 真实被引用的对象。
  • LedManagerProxy: 代理对象,通过特定模式能够与真实的对象产生联系。

代理模式主要是为客户端提供真实对象的使用入口,至于实现方式有多种,不必拘泥于某种特定的实现方法,达到代理模式的目的即可。

源码实现

「编程环境」

  1. 编译环境: Linux环境
  2. 语言: C 语言
  3. 编译命令: ./build.sh

「工程结构」

代码语言:javascript复制
Proxy/
├── build
│   └── build.sh
├── Client
│   ├── CMakeLists.txt
│   └── main_client.cc
├── CMakeLists.txt
├── Ipc
│   ├── CMakeLists.txt
│   ├── msg_manager.cc
│   └── msg_manager.h
├── Out
└── Server
    ├── Api
    │   ├── common_type.h
    │   ├── led_manager_proxy.cc
    │   └── led_manager_proxy.h
    ├── CMakeLists.txt
    ├── Led
    │   ├── led_manager.cc
    │   └── led_manager.h
    └── main_server.cc
  • build: 编译脚本
  • CMakeLists.txt: 编译工具
  • Client: 客户端进程代码,对应上文的B进程代码。
  • Ipc: 提供进程间通信的模块代码。
  • Server/Api: 代理对象的代码,对客户端进程开放。
  • Server/Led: 真实的Led控制对象代码。
  • Server/main_server.cc: 享有Led控制资源的进程代码,对应上文A进程。

此处将Ipc、Server/Api、Server/Led分别编译成动态库,其中Client只需链接Api的库就能控制Led。这么做可以避免代码上的耦合。


「代理接口」

代码语言:javascript复制
class CLedManagerProxy
{
public:
    CLedManagerProxy();

    ~CLedManagerProxy();

    void ShowHorseLight(int index);

    void ShowBreathLight(int index);

    void OpenLight(int index);

    void Stop(int index);
};

客户进程可通过上述接口实现对应Led的功能操作。由服务进程维护者提供头文件与库,被客户进程使用。

「客户进程」

代码语言:javascript复制
// main_client.cc
int main(int argc, char *argv[])
{
    char input = 0;
    CLedManagerProxy theLedManagerProxy;

    print_info();
    do {
        MAIN_LOG("Input case: ");
        input = fgetc(stdin);
        getchar();

        switch (input)
        {
            case 'a':
                theLedManagerProxy.OpenLight(LED1);
            break;

            case 'b':
                theLedManagerProxy.ShowHorseLight(LED1);
            break;

            case 'c':
                theLedManagerProxy.ShowBreathLight(LED1);
            break;

            case 'd':
                theLedManagerProxy.Stop(LED1);
            break;

            case 'h':
                print_info();
            break;

            default:
                MAIN_LOG("No this case (%c).n", input);
            break;
        }
    } while(input != 'q');
    return 0;
}

上述为客户进程,通过调用代理模式的接口完成对应Led功能的操作。


「服务进程」

代码语言:javascript复制
int main(int argc, char *argv[])
{
    CLedManager::GetInstance()->Init();
    return 0;
}

服务进程在实现自身业务外,需实现监听命令的线程,用于相应代理模式的申请。

「真实对象」

代码语言:javascript复制
class CLedManager
{
public:
    CLedManager();

    ~CLedManager();

    static CLedManager* GetInstance();

    void Init();

    static void SendMsg(int type, void* msg);

    void ProcessMsg(ELedMsgType type, void* msg);

    void ShowHorseLight(int index);

    void ShowBreathLight(int index);

    void OpenLight(int index);

    void Stop(int index);
};

上述为真实的操作Led的对象,直接控制硬件资源。


「Ipc通信」 由于代理对象在客户进程使用,真实对象在服务进程使用。需要通过进程间通信来实现两者的映射关系。

代码语言:javascript复制
// 发生请求
void SendMsgEvent(void *pEvent, int size)
{
    if (!pEvent) {
        LOGE("pEvent is NULL!n");
        return;
    }

    msg_send(pEvent, size);
}

上述接口用于代理对象发送命令,其实方式可采用管道、消息队列、共享内存或socket等其中一种即可。

代码语言:javascript复制
// 监听请求
void SetListener(PTypeCallBack pCb)
{
    SListenParam param;

    if (!pCb) {
        LOGE("pCb is NULL!n");
        return;    
    }

    param.cb = pCb;

    if (!init_flag) {
        init(&param.fd);
        init_flag = 1;
    }

    LOGD("Create listen_thread!n");
    thread t(listen_thread, &param);
    t.join();
}

此接口创建了一个线程用于时刻监听代理对象的请求,通过回调函数作出响应。

两个接口配合使用,此处实现方式采用的fifo方式。为了文章排版,不过多贴代码了。可在"开源519"公众号后台输入标题获取源码。


测试效果

测试.png

总结

  • 「代理模式」实现方式看上去是为客户重新封装一套与真实对象映射的接口,能够与正在运行的真实对象建立联系。
  • 可通过增删代理对象的接口,来控制客户或第三方对真实对象的访问范围。规避不希望客户或第三方使用的接口或实现细节。
  • 本例程的代码并没有完全按照代理模式的类图再实现一套代理对象与真实对象的接口基类,本代码看上去是不需要的。因此在引用设计模式时,并不需要完全按照其规定的方式来实现,理解其中的设计思想,结合自身情况,实现方式合理即可。
  • 代理模式在安卓中被广泛运用到,例如binder。感觉作用是希望能够与运行中的对象建立映射,如此一来便能够像正常调用一样完成交互。
  • 代理对象即使在真实对象未准备好时,也能够正常工作。
  • 本文只列举远程代理的例子,其他类型的代理原理大致相同。

最后

用心感悟,认真记录,写好每一篇文章,分享每一框干货。

更多文章内容包括但不限于C/C 、Linux、开发常用神器等,可进入“开源519公众号”聊天界面输入“文章目录” 或者 菜单栏选择“文章目录”查看。公众号后台聊天框输入本文标题,在线查看源码。

0 人点赞