C语言与设计模式之简单工厂模式
时隔不知道多久的颓废之后,正在尝试恢复人类形态(并非
在面向对象编程中,设计模式是解决常见问题的经典方案。然而,在C语言这种非面向对象的语言中实现设计模式需要一些技巧。本文将探讨如何在C语言中实现简单工厂模式,这是一种创建型设计模式,用于封装对象的创建过程。
项目结构
首先,让我们看一下这个简单示例的目录结构:
1 | ├── include/ |
挑战与思路
拿C语言写设计模式真是一整个牢住了,没有继承,没有多态,什么都没有。突然有点想念强面向对象语言。
用C语言实现设计模式确实有一定挑战,因为C语言本身不支持继承、多态等面向对象特性。但通过一些技巧,我们仍然可以模拟出类似的效果。
在这个简单工厂模式的实现中,我们使用了一些模拟面向对象编程的技术,包括不透明指针模式。这些技术可以帮助我们在C语言中实现类似面向对象的行为。
顺带一提,在这个简单工厂模式的实现里,有涉及到一些模拟oop跟不透明模式的东西,或许大概可能也许以后会写一下这些东西(逃
Shape句柄的实现
让我们先来看看shape_t这个句柄是如何定义的:
1 | typedef const struct shape_api **shape_t; |
shape_t 是一个指向 shape_api 结构体指针的指针。通过这种设计,我们可以使用 (*shape_t)->成员函数 的方式来访问不同形状类实现的成员函数。
工厂函数
在简单工厂模式中,工厂的主要职责是创建对象。我们实现了以下两个工厂函数:
1 | shape_t shape_create(enum shape_type type); |
其中 shape_create 是 shape_create_with_param 的简化版本,使用预定义的默认参数。让我们重点分析 shape_create_with_param 函数:
1 | shape_t shape_create_with_param(const shape_config_t *config) { |
这里没有什么错误处理,毕竟它只是一个简易的demo而已。
这个函数的主要逻辑是:
- 检查配置参数是否有效
- 根据配置中的类型创建相应的对象
- 调用对应类的初始化函数
- 返回对象的句柄
由于我没有实现自己的内存分配函数,所以我们这里就使用 malloc()。
配置结构体
工厂函数需要一个配置结构体来指定要创建的对象类型和参数:
1 | typedef struct shape_config { |
shape_config_t 结构体使用联合体(union)来存储不同类型形状的参数。这种设计使得我们可以用一个结构体来表示所有可能形状的配置。
基本上来说,shape_config_t基本上就只是一个单纯的结构体,包含了需要构建对象的类别跟所需要的参数
设计考虑
需要注意的是,当前的实现存在一个设计上的局限性:每次添加新的形状类型时,都需要修改 shape_config_t 结构体和 shape_create_with_param 函数。这违反了开闭原则(对扩展开放,对修改封闭)。
当然,这个配置结构体跟刚才提到的create函数都是不符合开闭原则的。每添加一个类都需要对他俩进行修改,这很不优雅。
在实际项目中,可以考虑更灵活的解决方案,比如使用注册机制或配置表来避免直接修改核心代码。不过,对于这个简单的演示来说,当前的实现已经足够清晰。
在拷打了一会AI之后,AI给了我一种新的解决方案,待我有空了去研究研究。
初始化函数
回归正题,我们发现这个工厂要求被它管理的类需要有”shape_rectangle_init_with_size”这样一个暴露出来的初始化函数
工厂要求每个被管理的类都提供一个公开的初始化函数。以矩形为例:
1 | void shape_rectangle_init_with_size(struct shape_rectangle *self, float width,float height) { |
随便掏一个出来,可以发现它所做的工作也并不复杂,只是很单纯的置0了对象,然后把传进来的参数填进去了而已。
初始化函数主要完成以下工作:
- 使用
memset将对象内存清零 - 将虚函数表指针绑定到对象的
api成员 - 设置对象的特定参数(如矩形的宽度和高度)
“self->shape.api = &_rectangle_api;”这一句可以稍微注意一下,这个初始化函数将对应子类的(基类为shape)虚函数表绑定到了self->shape.api,这是我们之后实现运行时多态的必要条件
虚函数表
每个形状类都有自己的虚函数表,定义了该类实现的特定行为:
1 | const struct shape_api _rectangle_api = {.draw = _shape_rectangle_draw, .destroy = _shape_rectangle_destroy}; |
虚函数表大概长这样,我们实现了draw跟destroy函数并且绑定到了这个结构体上
这个虚函数表包含了指向实际实现函数的指针。对于矩形类,我们实现了 _shape_rectangle_draw 和 _shape_rectangle_destroy 函数。
成员函数实现
让我们看看 _shape_rectangle_destroy 函数的实现:
1 | static void _shape_rectangle_destroy(shape_t shape) { |
这里把destroy掏出来看看,基本上来说它就只是单纯的用container_of宏,通过在rectangle子类中包含的shape*的地址和一些别的信息反推出了rectangle本身的地址,然后执行了free。其实draw也是差不多的,也是反推出来本身地址之后,取出结构体中的内容物进行使用,你甚至可以再次进行一个分层,这没什么问题。
这个函数使用 container_of 宏从 shape_t 句柄反推出完整的矩形对象地址,然后释放内存。draw 函数的实现原理类似,也是通过反推地址来访问对象的特定数据。
container_of 宏是一个常用的C语言技巧,它通过结构体成员的地址计算出整个结构体的地址。
通用接口函数
虽然严格来说,以下两个函数不属于工厂模式的核心部分,但它们提供了统一的接口来操作所有形状对象:
1 | void shape_draw(shape_t shape); |
虽然强行归类一切没什么意义,C语言甚至都不是面向对象的,更不是强面向对象。但是没法把他俩归类还是让我有点烦。
这些函数提供了类型安全的操作接口,隐藏了具体的实现细节。
shape_draw 函数的实现展示了如何通过虚函数表调用具体的实现:
1 | void shape_draw(shape_t shape) { |
他俩其实也挺简单的,就是进行一个简单的判空之后,解引用出*shape_api,然后再通过结构体首个成员的地址是整个结构体的地址这个机制套娃两次就可以对子类绑定好的成员函数进行一个调用了。
这个函数首先进行空指针检查,然后通过双重解引用访问虚函数表中的 draw 函数指针,最后调用具体的实现。这种设计允许我们在不知道具体类型的情况下调用正确的方法。
示例代码
下面是一个完整的使用示例,展示了如何创建和使用不同形状:
1 | int main(int argc, char **argv) { |
当然,最后在这里赋上一个main.c
总结
通过这个简单的示例,我们可以看到如何在C语言中实现简单工厂模式。虽然C语言本身不支持面向对象特性,但通过一些技巧,我们仍然可以模拟出类似的效果:
- 不透明指针:使用
shape_t作为不透明句柄,隐藏实现细节 - 虚函数表:通过结构体指针模拟虚函数表,实现运行时多态
- 工厂函数:封装对象的创建过程,提供统一的创建接口
- 配置结构体:使用联合体存储不同类型对象的参数
感觉这种设计模式,复杂的已经不是单个成员函数了,复杂的是这些函数之间的关系了感觉。
这种设计模式的复杂性主要来自于函数之间的关系和交互,而不是单个函数的实现。通过合理的抽象和封装,我们可以在C语言中构建出灵活、可扩展的系统。
在实际项目中,可以根据需要进一步优化这个实现,比如添加更完善的错误处理、支持动态注册新类型、或者实现更复杂的设计模式。