过程语言确实有设计模式。但由于程序化方法通常被忽略,而倾向于基于类的 OOP,因此它们并未得到广泛认可。
我用 C 开发高性能软件,并且有几种重复出现的模式。因此,我将提供一些我经常看到的模式的见解。
手柄
这就是在过程编程中进行封装的方式。构造函数不返回结构或对象。但是一个句柄:它通常是一个不透明的指针或只是一个整数。你不能用它做任何有趣的事情,因为它只是一个数字。细节完全隐藏。但是您可以将此句柄传递给处理它的函数:
例子:
- 在 Windows 上,CreateWindow 函数返回 HWND。这是一个窗口句柄,可以传递给其他函数,如 ShowWindow、DestroyWindow 等。
- 在 Linux 上,open 系统调用。返回just和int。这是一个文件句柄。
上下文
在过程语言中,对象通常被称为上下文。上下文是一个包含某些系统状态的结构,就像对象的成员一样。在 OOP 中,您编写 object.method(parameter)。在过程编程中,您编写function(addressOfContext, parameter)。内部函数直接使用上下文结构,而公共函数仅使用句柄,实现将其解析为实际的上下文结构。
回调
或函数指针。函数的用户传递他的函数地址以向系统添加自定义行为。这就是多态性在过程编程中的实现方式。这允许编写泛型函数。
一个值得注意的例子是qsort C 函数。这需要元素数组的地址。获取一个元素的大小以及数组中有多少元素以及执行比较的比较器函数。这是完全通用的实现,允许对各种数据进行排序。
设置结构
当一个函数可以通过多种方式进行参数化时。通常使用设置结构。
规范通常要求这些结构默认填充为零,并且只填充相关成员。如果某些成员是互斥的,则将它们置于联合中。这种设置结构的典型示例是来自 WinAPI 的WNDCLASS。
可变尺寸数据
这与其说是通用设计模式,不如说是一种 C 模式。有时对象可能包含任意大小的二进制有效负载。这种模式通常发生在从包含多种类型数据块的二进制文件中读取数据时。这是由这样的结构完成的。
typedef struct
{
int someData;
int otherData;
int nPayloadLength;
unsigned char payload[1];
} VariableSized;
在代码中完成了以下操作:
VariableSized *vs = malloc(sizeof(VariableSized) + extraLength);
这会分配比结构更大的内存,从而为可变长度的有效负载留出空间。然后可以通过例如访问其第5个字节。 vs->payload[4].
这样做的好处是可以在一次free 调用中释放整个对象。并且保证它在内存中有一个连续的块。所以它比在堆中的其他地方分配相应的缓冲区更好地利用缓存。
OOP 设计模式的过程对应物
OOP 模式在过程语言中从不以它们的名字命名。所以我只能在这里猜测。
创作模式
-
抽象工厂:抽象工厂一般是单例的。在这种情况下,根本不使用此模式,而是使用条件编译。否则设置提供创建功能的结构。
-
Builder:使用设置结构。
-
工厂方法:回调用于创建。
-
延迟初始化:在 C++ 中,静态局部变量用于此目的。在 C 中,您可以在对性能不重要的地方使用
if (!initialized) { initialize(); initialized = 1; } 模式。对于性能关键代码,根本不使用延迟加载。用户必须找到一个地方来初始化上下文。
-
原型:在程序世界中,我们只需将句柄返回给库存对象。 WinAPI 中的GetStockObject 函数就是一个例子。对于可变对象,出于性能原因,通常使用写时复制机制。
-
单例:只需编写顶级函数(并在绝对需要全局状态时使用全局变量)。
结构模式
-
Adapter 和 Facade:在现有接口上构建另一个接口的模式。只需新函数将调用旧函数和其他函数。
-
Bridge:对具体实现的回调以 setup struct 的形式提供。
-
复合:使用顶级函数指定其应操作的父节点的句柄。
-
装饰器:装饰行为以回调的形式提供。或者为接收各种消息并决定是否处理它们的所有可能的装饰提供一个事件处理程序回调(例如 WinAPI 中的窗口过程)。
-
享元:用于封装在结构和数组中的只读二进制数据。
-
代理:与 OOP 中的几乎相同,但没有类。
行为模式
-
责任链:循环遍历的回调数组或链表。规范描述了回调应该如何表明他们处理了导致循环中断的请求。
-
Command:命令是包含
do 和undo 回调的结构。这些回调通常需要某种上下文来操作。并维护一系列命令来执行撤消操作。
-
解释器:使用 lex 和 yacc 编写或生成编译器/解析器/解释器。
-
迭代器:使用句柄,否则相同。在 C 语言中,出于性能原因,我们经常坚持使用数组。
-
Mediator:通常使用一些消息分发机制和message loops 和事件处理程序来实现。
-
Memento:与 OOP 中的相同,但没有类。
-
观察者:与责任链相同,但循环不会中断。一个例子是atexit。
-
状态:由二维调度表实现,将当前状态和请求的操作映射到一个函数中。 (在稀疏的情况下,只使用 if。)
-
策略:这是回调的基本用例。
-
模板方法:通常框架允许用户为某些函数提供自己的回调。库通常提供一种使用自定义内存分配函数的方法,该函数提供自定义
malloc 和free。
-
访问者:通过使用多维回调数组实现,通常在开始时填充 NULL(默认行为),并填充到每个类型对的主初始化代码中。