一、设计模式概述:什么是设计模式、为什么C语言也需要设计模式、面向过程与面向对象的设计思想对比
1.1 什么是设计模式?说白了就是“套路”
我刚开始做嵌入式开发那会儿,总觉得“设计模式”是Java、C++那些高级语言的专利。C语言嘛,直接撸代码就完事了。后来被现实狠狠教育了一回——一个项目写到两万行,代码乱得像一锅粥,改一个bug能引出三个新bug。那时候我才意识到:写代码容易,写好代码难。
设计模式是什么?说白了就是前辈们在长期实践中总结出来的“套路”。它不是具体的代码,而是一种解决问题的思路。就像下棋有定式,盖房子有图纸,写代码也有它的“模式”。
设计模式的核心定义:在特定场景下,针对反复出现的问题,给出的可复用的解决方案。
举个例子。你写一个LED驱动,今天用GPIO控制,明天客户说要用PWM调光。如果你把控制逻辑和硬件操作写死在一起,那改起来就痛苦了。但如果你用了一个“接口抽象”的模式,把“控制”和“实现”分开,换硬件就像换灯泡一样简单。这就是设计模式的价值。
1.2 为什么C语言也需要设计模式?
有人会问:“C语言不是面向过程的吗?设计模式不是面向对象的专利吗?”
嗯,这个问题我当年也纠结过。其实设计模式不依赖语言,它依赖的是你的思维方式。C语言虽然没有class、继承、多态这些语法糖,但我们可以用结构体、函数指针、宏定义等手段,实现同样的效果。
我在项目中遇到过这样一个场景:一个通信协议栈,需要支持UART、SPI、I2C三种物理层。如果用面向过程的方式写,就是三个大switch-case,每个case里塞一堆硬件操作代码。后来我用了“策略模式”的思路,把每种物理层的收发函数封装成函数指针,挂在结构体里。调用方只关心“发送数据”,不关心“怎么发送”。
你看,这不就是设计模式吗?
我的建议:在嵌入式C语言开发中,设计模式不是“要不要用”的问题,而是“怎么用好”的问题。它能帮你写出更易维护、更易扩展、更易测试的代码。
为什么C语言特别需要设计模式?因为嵌入式项目有几个特点:
- 资源受限:不能随便new对象,不能滥用动态内存。设计模式能帮你用最小的开销,获得最大的灵活性。
- 硬件耦合高:代码和硬件绑得太紧,换芯片就像换心脏。设计模式能帮你解耦,让代码“活”起来。
- 长期维护:嵌入式产品生命周期长,动辄五到十年。没有好的架构,后期维护就是噩梦。
1.3 面向过程 vs 面向对象:不是对立,是互补
很多人把面向过程和面向对象对立起来,觉得C语言就是面向过程,C++就是面向对象。其实没那么绝对。
面向过程的核心思想是:数据 + 算法。你定义一些数据结构,然后写一堆函数去操作它们。代码的流程是线性的,从main()开始,一步步往下走。
面向对象的核心思想是:对象 + 消息。你把数据和操作数据的方法打包在一起,形成一个“对象”。对象之间通过“消息”(也就是函数调用)来交互。
我个人的理解是:面向过程关注“怎么做”,面向对象关注“谁来做”。
举个例子。你要控制一个电机:
- 面向过程:写一个motor_set_speed(int speed)函数,里面直接操作寄存器。谁调用它,谁就负责传参数。
- 面向对象:定义一个Motor结构体,里面包含速度、方向、状态等数据,以及set_speed、stop、get_status等函数指针。每个电机实例有自己的“行为”。
你看,C语言完全可以用结构体+函数指针的方式,模拟出“对象”的效果。这就是我们这门课要讲的核心——用C语言实现设计模式。
注意:不要为了用模式而用模式。我曾经见过一个项目,硬生生把简单的LED控制写成了“工厂模式+观察者模式+状态机模式”,代码量翻了三倍,可读性反而下降了。设计模式是工具,不是目的。
1.4 本章知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一个“地图”,后面每讲一个模式,都能在这张图上找到它的位置。
1.5 一个简单的例子:用C语言实现“封装”
说了这么多理论,不如看一段代码。下面这个例子,展示了如何用C语言实现“封装”——把数据和操作打包在一起。
// 传统的面向过程写法
int motor_speed = 0;
void motor_set_speed(int speed) {
motor_speed = speed;
// 直接操作硬件寄存器
// REG_SPEED = speed;
}
// 使用设计模式思想的写法
typedef struct {
int speed;
int direction;
void (*set_speed)(struct Motor *self, int speed);
void (*stop)(struct Motor *self);
} Motor;
void motor_set_speed_impl(Motor *self, int speed) {
self->speed = speed;
// 操作硬件寄存器
// REG_SPEED = speed;
}
void motor_stop_impl(Motor *self) {
self->speed = 0;
// 停止电机
// REG_SPEED = 0;
}
// 初始化一个电机对象
Motor motor1 = {
.speed = 0,
.direction = 1,
.set_speed = motor_set_speed_impl,
.stop = motor_stop_impl
};
// 使用
motor1.set_speed(&motor1, 100);
你看,第二种写法虽然代码量多了几行,但好处很明显:
- 可扩展:再增加一个电机,只需要再初始化一个Motor结构体实例。
- 可替换:如果换了硬件,只需要改motor_set_speed_impl和motor_stop_impl这两个函数,调用方完全不受影响。
- 可测试:可以轻松地写一个“模拟电机”,用于单元测试。
一个小技巧:在实际项目中,我习惯把这种“对象”的定义放在头文件里,把实现放在.c文件里。这样调用方只看到接口,看不到实现细节——这就是“信息隐藏”。
1.6 避坑指南
我曾经在一个项目里,把设计模式用“过”了。一个简单的按键扫描,我硬是搞出了“状态模式+观察者模式+命令模式”。结果代码写出来,连我自己都看不懂了。后来重构,只用了一个简单的状态机,代码量减少60%,可读性反而更好。
所以,记住三条原则:
- 够用就好:不要为了用模式而用模式。一个if-else能解决的问题,别硬上策略模式。
- 先有需求,后有模式:模式是解决问题的,不是制造问题的。先搞清楚你要解决什么问题,再去找对应的模式。
- 模式是活的:不要死记硬背GoF的23种模式。在实际项目中,你可能会创造出自己的“模式”。
嗯,这一章就到这里。设计模式不是玄学,它就是你写代码时的一种“思维方式”。后面我们会一个一个地拆解,看看在C语言里,这些模式到底怎么落地。