什么是适配器模式?
核心思想:适配器模式是一种结构型设计模式,它允许不兼容的接口能够协同工作,就像现实世界中的电源适配器(插头转换器)能让你的美规电器在中规插座上使用一样,在软件中,适配器模式可以“包装”一个类的接口,使其看起来像是另一个接口,从而让原本无法交互的两个类能够连接起来。

目的:转换接口,而不是转换功能,它不会改变被包装对象的核心逻辑,只是提供一个“翻译层”。
适配器模式的应用场景
在 C 语言中,适配器模式通常用于以下几种情况:
- 封装第三方库:当你使用的第三方库(如某个网络库、数据库驱动)的接口不符合你项目现有的代码风格或架构时,你可以创建一个适配器,将第三方库的复杂接口封装成你项目中简单、统一的接口。
- 统一不同来源的数据结构:你的程序可能需要处理来自不同模块的数据,这些模块的数据结构可能略有不同(一个用
struct A,另一个用struct B,但它们的核心字段相似),你可以为每个数据源创建一个适配器,将它们统一转换成你程序内部的标准数据结构。 - 保持向后兼容性:当你重构或升级一个旧模块的接口时,为了不影响其他依赖该模块的代码,可以先创建一个适配器,将新接口“伪装”成旧接口,待所有代码都更新完毕后,再逐步移除适配器。
- 模拟面向对象的多态:C 语言没有原生的类和继承,但可以通过函数指针和结构体来模拟,适配器模式可以巧妙地利用这一点,让不同的函数实现能够以统一的方式被调用。
适配器模式的两种主要实现方式
在 C 语言中,实现适配器模式主要有两种方式:类适配器和对象适配器。
对象适配器 - 最常用
这种方式通过组合来实现,适配器结构体内部包含一个指向被适配对象的指针,这是最灵活、最常用的方法,因为它只需要一个指向被适配对象的指针,而不需要知道其完整定义。

示例:封装一个不同风格的排序函数
假设我们有一个旧的排序函数,它的签名很传统:
// 旧版排序函数 (被适配者)
// qsort 的标准签名
int compare_ints(const void* a, const void* b) {
int arg1 = *(const int*)a;
int arg2 = *(const int*)b;
return (arg1 > arg2) - (arg1 < arg2); // 简单的比较
}
void old_sort(int* array, int size) {
qsort(array, size, sizeof(int), compare_ints);
printf("Using old qsort style.\n");
}
我们希望在项目中使用一个更现代、更“面向对象”风格的排序接口,比如这样:
// 期望的新接口 (目标接口)
// 一个包含函数指针的结构体,模拟一个“排序器”对象
typedef struct {
void (*sort)(void* array, int size, int element_size, int (*compare)(const void*, const void*));
} ModernSorter;
void modern_sort_interface(void* array, int size, int element_size, int (*compare)(const void*, const void*)) {
printf("Using modern sort interface.\n");
// 在实际应用中,这里可能会调用更高效的算法
qsort(array, size, element_size, compare);
}
这两个接口不兼容,我们创建一个对象适配器,让 old_sort 能够像 ModernSorter 一样被使用。
// 适配器实现
// 1. 定义适配器结构体,它持有一个被适配者的实例(这里我们不需要,因为 old_sort 是全局函数)
// old_sort 是某个结构体的方法,这里就需要一个指向该结构体的指针。
// 2. 定义一个适配器函数,该函数实现了目标接口(ModernSorter),内部调用被适配者的方法。
// 适配器函数,它的签名匹配 ModernSorter.sort
void adapter_sort(void* array, int size, int element_size, int (*compare)(const void*, const void*)) {
printf("Adapter: Translating modern call to old qsort.\n");
// 直接调用被适配的函数
old_sort((int*)array, size);
}
// 创建一个“现代排序器”实例,但实际上它使用我们的适配器
ModernSorter create_legacy_sort_adapter() {
ModernSorter sorter;
sorter.sort = adapter_sort; // 将适配器函数赋值给函数指针
return sorter;
}
如何使用:
#include <stdio.h>
#include <stdlib.h>
// ... (上面的 old_sort, ModernSorter, adapter_sort, create_legacy_sort_adapter 代码) ...
int main() {
int data[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(data) / sizeof(data[0]);
// 使用适配器
ModernSorter my_sorter = create_legacy_sort_adapter();
printf("--- Before Sorting ---\n");
for (int i = 0; i < n; i++) {
printf("%d ", data[i]);
}
printf("\n");
// 调用方式完全符合“现代接口”
my_sorter.sort(data, n, sizeof(int), compare_ints);
printf("\n--- After Sorting ---\n");
for (int i = 0; i < n; i++) {
printf("%d ", data[i]);
}
printf("\n");
return 0;
}
输出:
--- Before Sorting ---
64 34 25 12 22 11 90
Adapter: Translating modern call to old qsort.
Using old qsort style.
--- After Sorting ---
11 12 22 25 34 64 90
在这个例子中,my_sorter 看起来像一个现代化的排序器,但它的 sort 方法实际上是由我们的 adapter_sort 函数实现的,而这个函数内部又调用了古老的 old_sort。adapter_sort 就是那个“翻译官”。
类适配器 - 较少使用
这种方式通过多继承来实现,在 C++ 中,适配器类可以同时继承自目标接口和被适配者,但在 C 语言中,没有继承,所以实现起来非常笨拙且不推荐,它意味着将被适配者的结构体直接嵌入到适配器的结构体中。
这会破坏封装性,因为适配者需要知道被适配者的完整内部结构,在 C 语言中,几乎总是优先选择对象适配器。
优缺点总结
优点
- 提高复用性:可以让原本由于接口不兼容而无法一起工作的代码协同工作。
- 增加灵活性:可以随时替换被适配者,只要新的被适配者也能被同一个适配器“翻译”即可。
- 符合开闭原则:你可以通过引入新的适配器来扩展系统的功能,而无需修改现有代码。
- 封装性:客户端代码只与目标接口交互,完全不知道被适配者的存在和复杂性。
缺点
- 增加复杂性:会引入额外的间接层(适配器),使得系统调用链变长,可能增加理解和调试的难度。
- 性能开销:虽然通常很小,但额外的函数调用会带来微小的性能损耗。
- 过度使用:如果接口不兼容的问题可以通过重构代码来解决,那么使用适配器可能会掩盖设计上的缺陷。
在 C 语言中,适配器模式是一个强大的工具,主要用于接口转换,虽然它不像在 C++ 或 Java 中那样有语法糖支持,但通过结构体和函数指针的组合,可以非常灵活地实现。
核心要点:
- 目标:让不兼容的接口协同工作。
- 常用实现:对象适配器(组合),即用一个结构体包装被适配者的指针,并提供一个符合目标接口的函数。
- 应用:封装第三方库、统一数据接口、保持向后兼容等。
- 原则:优先使用组合而非“继承”(在C中表现为嵌入结构体),以保持良好的封装性。
当你发现两段代码“对不上话”时,不要急于修改其中一方,先考虑是否可以引入一个适配器来优雅地解决问题,这是一种非常专业和可维护的做法。
