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 代理模式的核心逻辑
下面这张图展示了代理模式的核心流程。你可以看到,调用者始终面对的是代理接口,代理在背后决定是否、何时、如何调用真实对象。
13.8 避坑指南
我曾经在代理模式上栽过跟头,分享几个教训:
- 代理不要变成“二传手”:如果代理只是简单转发,没有任何额外逻辑,那它就是多余的。代理必须提供价值。
- 小心代理链过长:一个代理套一个代理,调用链越来越长,性能越来越差。我见过一个系统,一个函数调用经过5层代理,调试起来简直噩梦。
- 代理和真实对象的生命周期要匹配:真实对象被销毁了,代理还在用,那就是野指针。我建议代理持有真实对象的引用计数,或者用弱引用。
- 不要滥用代理:不是所有地方都需要代理。简单的函数调用,加个代理反而画蛇添足。
总结一下:代理模式是个好工具,但要用对地方。访问控制、延迟加载、日志记录,这三个场景是代理模式的“舒适区”。用好了,代码更健壮、更高效;用不好,就是给自己挖坑。
嗯,代理模式就讲到这里。记住一句话:代理不是替代,而是增强。它让真实对象更安全、更高效、更可追溯。