13、代理模式:访问控制、延迟加载、日志代理

代理模式,说白了就是给一个对象找个“替身”。你想想看,有些对象很“重”,创建起来费时费力;有些对象很“敏感”,不能随便让人碰;还有些对象需要“留痕”,每次调用都得记个日志。这时候,代理就派上用场了。

我个人习惯把代理模式理解为“中间人”。它不改变原始对象的行为,而是在调用前后插入一些额外的逻辑。嗯,这有点像房产中介——你买房还是那个房,但中介帮你做了筛选、带看、签约这些事。

13.1 为什么需要代理?

我在项目中遇到过这样一个场景:一个图像处理库,加载高清图片特别慢。每次启动都要加载几十张图,用户等得直骂娘。后来我用代理模式做了个“懒加载”,只有图片真正要显示时才去加载。效果立竿见影。

代理模式的核心价值有三个:

  • 访问控制:谁可以调用?什么时候可以调用?
  • 延迟加载:真正需要时才创建对象,节省资源。
  • 日志记录:每次调用都留下痕迹,方便调试和审计。

这三种场景,在嵌入式开发中尤其常见。你想想看,嵌入式设备资源有限,能省一点是一点。

13.2 代理模式的结构

代理模式的结构其实很简单。它通常包含三个角色:

  • 抽象主题(Subject):定义真实对象和代理的共同接口。
  • 真实主题(RealSubject):真正干活的类。
  • 代理(Proxy):持有真实主题的引用,控制对它的访问。

在C语言中,我们通常用函数指针和结构体来实现这种关系。没有C++的虚函数,我们就用回调函数来模拟多态。

核心思想:代理和真实对象实现相同的接口。调用者不需要知道自己在跟代理打交道还是跟真实对象打交道。

13.3 访问控制代理

访问控制代理,说白了就是“看门狗”。它检查调用者的权限,决定是否放行。

我曾经做过一个车载系统,底层硬件接口不能随便让应用层调用。万一哪个应用乱写寄存器,车机可能直接死机。于是我在硬件驱动外面包了一层代理,只有经过授权的模块才能访问。

// 抽象接口
typedef struct {
    void (*write)(uint32_t addr, uint32_t data);
    uint32_t (*read)(uint32_t addr);
} HardwareOps;

// 真实硬件操作
static void real_write(uint32_t addr, uint32_t data) {
    // 直接操作硬件寄存器
    *(volatile uint32_t *)addr = data;
}

static uint32_t real_read(uint32_t addr) {
    return *(volatile uint32_t *)addr;
}

// 代理:带访问控制
typedef struct {
    HardwareOps ops;
    int (*check_permission)(const char *module);
    const char *module_name;
} SecureProxy;

static void proxy_write(uint32_t addr, uint32_t data) {
    SecureProxy *proxy = get_current_proxy();
    if (!proxy->check_permission(proxy->module_name)) {
        printf("Access denied: %s\n", proxy->module_name);
        return;
    }
    real_write(addr, data);
}

// 使用方式
SecureProxy proxy = {
    .ops.write = proxy_write,
    .ops.read = proxy_read,
    .check_permission = auth_check,
    .module_name = "media_player"
};

注意:访问控制代理不能替代底层安全机制。它只是软件层面的防护,硬件级别的保护(如MPU、MMU)才是最后一道防线。

13.4 延迟加载代理

延迟加载,也叫懒加载。对象不是一开始就创建,而是等到第一次使用时才创建。这在嵌入式系统中特别有用——内存就那么点,不能什么都提前准备好。

我记得有个项目,需要加载一个大型配置文件。这个文件只在特定模式下才用到,但系统启动时却总是加载它。我用代理模式改成了按需加载,启动时间从8秒降到了2秒。

// 延迟加载代理
typedef struct {
    int (*get_data)(int key);
    int (*is_loaded)(void);
} ConfigProxy;

static Config *real_config = NULL;

static int lazy_get_data(int key) {
    if (real_config == NULL) {
        printf("Loading config on demand...\n");
        real_config = load_config_from_flash();
    }
    return real_config->data[key];
}

static int lazy_is_loaded(void) {
    return real_config != NULL;
}

ConfigProxy config_proxy = {
    .get_data = lazy_get_data,
    .is_loaded = lazy_is_loaded
};

小技巧:延迟加载别忘了考虑线程安全。如果多个任务可能同时触发加载,记得加锁。我吃过这个亏,两个任务同时触发加载,结果内存泄漏了。

13.5 日志代理

日志代理,就是给每个函数调用记个“流水账”。调试的时候特别有用。尤其是那些“偶发”的bug,你盯着它看它就不出现,你一转身它就来了。有了日志代理,至少能知道调用顺序和参数。

// 日志代理
typedef struct {
    void (*send)(const char *msg);
    int (*recv)(char *buf, int len);
} CommOps;

// 真实通信接口
static void real_send(const char *msg) {
    UART_Send(msg, strlen(msg));
}

// 日志代理
static void log_send(const char *msg) {
    printf("[LOG] send() called, msg=%s\n", msg);
    real_send(msg);
    printf("[LOG] send() finished\n");
}

CommOps comm_with_log = {
    .send = log_send,
    .recv = log_recv
};

嗯,这里要注意:日志代理不能影响性能。如果每个函数调用都打印日志,系统可能被拖慢。我一般会加一个日志级别开关,生产环境只打印错误日志,调试环境才打印详细信息。

13.6 三种代理的对比

代理类型 核心目的 典型场景 注意事项
访问控制代理 权限校验 硬件接口保护、多用户系统 不能替代硬件安全机制
延迟加载代理 资源节省 大文件加载、初始化耗时操作 注意线程安全
日志代理 行为记录 调试、审计、性能分析 注意性能开销

13.7 代理模式的核心逻辑

下面这张图展示了代理模式的核心流程。你可以看到,调用者始终面对的是代理接口,代理在背后决定是否、何时、如何调用真实对象。

代理模式核心逻辑 调用者 代理 真实对象 调用 转发/控制 代理的职责 1. 访问控制:权限检查 2. 延迟加载:按需创建 3. 日志记录:调用跟踪 4. 缓存结果:避免重复计算 5. 性能监控:统计耗时 调用者只与代理交互,不直接接触真实对象

13.8 避坑指南

我曾经在代理模式上栽过跟头,分享几个教训:

  • 代理不要变成“二传手”:如果代理只是简单转发,没有任何额外逻辑,那它就是多余的。代理必须提供价值。
  • 小心代理链过长:一个代理套一个代理,调用链越来越长,性能越来越差。我见过一个系统,一个函数调用经过5层代理,调试起来简直噩梦。
  • 代理和真实对象的生命周期要匹配:真实对象被销毁了,代理还在用,那就是野指针。我建议代理持有真实对象的引用计数,或者用弱引用。
  • 不要滥用代理:不是所有地方都需要代理。简单的函数调用,加个代理反而画蛇添足。

总结一下:代理模式是个好工具,但要用对地方。访问控制、延迟加载、日志记录,这三个场景是代理模式的“舒适区”。用好了,代码更健壮、更高效;用不好,就是给自己挖坑。

嗯,代理模式就讲到这里。记住一句话:代理不是替代,而是增强。它让真实对象更安全、更高效、更可追溯。

公众号:蓝海资料掘金营,微信 deep3321