软件设计中最折磨人的,不是找不到方案,而是方案不知道该停在哪个复杂度上。我见过把一个状态机“简化”成三个全局布尔变量的源码,也见过一个字符串格式化函数被套上四层抽象工厂的项目——前者死于过度简化,后者崩在过度复杂化。这两条路我都在真实项目里走过头,摔过跟头,也花过很长时间琢磨一个问题:为什么大家明明知道“适度最好”,却还是无可避免地滑向其中一个极端。
最近“嵌入式软件设计基础”的热度又上来了,恰好这个领域是两极化翻车事故最高发的地方。硬件资源有限时,人会下意识砍设计;硬件一升级、需求一变多,又会报复性地堆设计。这篇文章不打算给什么万能答案,而是把我这些年观察到、踩过的两类极端好好拆一遍:它们各自长什么样、背后是什么动机在驱动、怎么在项目里提前识别,以及我目前认为比较可行的平衡做法。
1. 过度简化:像一个没有排水管的高架桥
1.1 全局布尔状态机是怎么把项目拖垮的
我接手过一块老产品的维护,它的核心逻辑是一个“精简”到极致的按键状态机。刚看到代码时我心里还感叹了一句:写得真短,没有多余的东西。
c复制/* v0,当时的“精简设计” */
static int state = 0;
void on_button(void)
{
state = (state + 1) % 3;
if (state == 0) {
led_green();
} else if (state == 1) {
led_yellow();
} else {
led_red();
}
}
三个状态循环切换,三行逻辑,确实很干净。但第二个月需求就变了:新增“长按两秒直接切换常亮模式”“离开待机界面超过十分钟自动回到状态0”“每种状态下按键短按和长按行为不同”。我开始往这段代码里打补丁,先是加了 prev_state 保存上次状态,然后加了 sustain_ms 计时,接着用 enabled_flag 控制某些状态下的响应,最后连 fault_code 都塞进这个全局状态里了。
到第五个版本时,这个“精简设计”已经变成了一个拥有十几个全局变量、到处互相写值的泥潭。最讽刺的是,最初那个 state 变量已经没有人能准确说出它的全部取值了。
这段经历让我想明白一件事:过度简化省掉的不只是设计,而是省掉了“让代码能承载变化”的那个结构。变化不会因为你没做准备就不来,它只会以更丑陋的方式钻进你的系统里。你省掉的结构,最后都会以更多不可读的变量、更多隐式耦合、更多“不知道改了哪里会炸”的形式补回来。
1.2 简化过头的几个典型症状
真正危险的过度简化,不是那种“写得太随便”的代码,而是那种看起来“很漂亮、很直接、几乎不需要注释”的代码。它的问题藏在暗处。
第一个典型症状是全局状态满天飞。不同模块之间靠几个全局数组或者公共变量通信,表面上看“这样传参多方便”,实际上所有模块的边界都被凿穿了。我见过最夸张的例子是一个通信协议栈,所有消息都往同一个缓冲区里写,靠 current_sender 和 last_message_type 来区分是谁的消息。任何一个小函数都可能覆盖缓冲区里的数据,排查两个模块互相踩数据的bug,一查就是好几天。
第二个症状是错误处理被省略成“不会出错”。很多简化过头的代码里,函数返回值要么没人检查,要么全部返回 -1 表示“反正挂了”。网络收发、外部存储读写、协议解析,这些天生就可能失败的环节被当成绝不会失败来处理。等产品在现场真的遇到异常输入,表现就是随机死机或者数据错乱,查都查不到。
第三个症状是业务规则被揉进了“看似通用”的代码里。比如在字符串处理函数里偷偷判断 if (len == 4) { special_case(); },在循环里用魔法数字 3 表示“三种状态”。这些代码在写的当下“零成本”,但每一处都是一个时间炸弹。
我给这种做法的总结是:它就像一座高架桥没设计排水系统。通车头几个月阳光明媚,怎么跑都没事;等雨季一来,路面积水、桥体开裂、全线瘫痪,返工成本远超当初那根排水管的价格。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
