第二章:架构设计原则——模块化设计思想、高内聚低耦合、接口隔离原则、分层架构模式
各位同学,咱们接着聊。上一章我讲了超声系统的整体轮廓,这一章咱们深入一点,聊聊架构设计的几个核心原则。说白了,这些原则就是指导你「怎么把代码组织得漂亮、好改、不容易崩」的几条铁律。
我做了十几年嵌入式,见过太多「屎山」代码——一个函数两千行,全局变量满天飞,改一个bug冒出三个新bug。嗯,这些坑我都踩过。今天我把几个最实用的原则掰开揉碎讲给你听。
2.1 模块化设计思想:拆,是解决问题的第一步
模块化设计,说白了就是「分而治之」。你想想看,一个超声系统几十万行代码,如果全塞在一个文件里,谁看得懂?谁敢改?
我个人习惯,拿到需求第一件事不是写代码,而是画模块图。把系统拆成若干个功能独立的模块,每个模块只干一件事,干好一件事。
模块化的核心三问:
- 这个模块的输入是什么?输出是什么?
- 这个模块对外暴露哪些接口?
- 这个模块内部的变化,会不会影响其他模块?
举个例子,超声系统的波束合成模块。它只负责从原始射频数据计算出波束合成后的数据。它不关心数据是从哪个探头来的,也不关心图像怎么显示。输入是通道数据,输出是波束合成结果,中间全是内部算法。这就是一个合格的模块。
我在项目中遇到过最典型的反面教材:有人把波束合成、图像滤波、测量计算全写在一个类里。结果换了一个探头型号,波束合成参数要调,结果把图像滤波的逻辑也改崩了。你说冤不冤?
2.2 高内聚低耦合:模块内部的「紧」与模块之间的「松」
这两个词你肯定听过,但真正理解的人不多。我换个说法:高内聚,就是一个模块内部的东西,应该高度相关,拧成一股绳。低耦合,就是模块之间,尽量少依赖,尽量松一点。
为什么会这样?因为耦合越紧,牵一发而动全身。你改A模块,B模块跟着崩,C模块也报错,最后你发现改一个功能要动十个文件。这种项目我见过太多。
| 耦合类型 | 表现 | 危害程度 |
|---|---|---|
| 内容耦合 | 一个模块直接修改另一个模块的内部数据 | 极高,应绝对避免 |
| 公共耦合 | 多个模块共享同一个全局变量 | 高,尽量少用 |
| 控制耦合 | 一个模块通过参数控制另一个模块的执行流程 | 中等,可接受但需谨慎 |
| 数据耦合 | 模块之间只传递简单数据 | 低,推荐使用 |
你看,数据耦合是最理想的。模块A调用模块B,只传几个参数,拿回一个结果。至于B内部怎么实现的,A完全不用管。这就是低耦合。
高内聚的例子呢?我拿超声的「图像后处理」模块来说。它内部包含增益调节、动态范围压缩、边缘增强等功能。这些功能都围绕「改善图像质量」这一个目标,彼此之间数据流转紧密。这就是高内聚。如果你把「网络通信」也塞进这个模块,那就乱套了。
一个小技巧:写代码时,如果一个函数超过50行,或者一个类超过10个方法,你就该停下来想想——是不是内聚不够?是不是该拆了?
2.3 接口隔离原则:别让调用者看到不该看的东西
这个原则,我理解起来就一句话:接口要小而专,不要大而全。
你想想看,一个超声系统的「探头控制模块」,它需要对外提供什么接口?无非就是:初始化探头、设置发射参数、读取回波数据。这就够了。如果你把「校准探头」「更新固件」「读取温度传感器」这些内部操作也暴露出去,那调用者就懵了——我到底该用哪个?
我曾经接手过一个项目,一个类暴露了40多个公共方法。调用者根本不知道哪些是核心接口,哪些是内部辅助函数。结果就是,每个人都在用不同的方式调用,代码乱成一锅粥。
接口隔离原则要求我们:每个接口只服务于一个特定的调用者或场景。比如,图像显示模块只需要知道「获取当前帧数据」这个接口,它不需要知道波束合成模块的「设置焦点深度」接口。
// 不好的设计:一个大接口
class ProbeController {
public:
void init();
void setTxParams(int freq, int voltage);
void startAcquisition();
void calibrate(); // 内部操作,不该暴露
void updateFirmware(); // 内部操作,不该暴露
void readTemperature(); // 内部操作,不该暴露
RawData getRxData();
};
// 好的设计:接口隔离
class IProbeControl {
public:
virtual void init() = 0;
virtual void setTxParams(int freq, int voltage) = 0;
virtual void startAcquisition() = 0;
virtual RawData getRxData() = 0;
};
class IProbeMaintenance {
public:
virtual void calibrate() = 0;
virtual void updateFirmware() = 0;
virtual float readTemperature() = 0;
};
你看,把维护接口和业务接口分开,调用者各取所需。图像模块只依赖 IProbeControl,维护工具只依赖 IProbeMaintenance。互不干扰。
注意:接口隔离不是让你把接口拆得越碎越好。拆得太碎,调用者要组合多个接口才能完成一个功能,反而增加了复杂度。适度隔离,恰到好处。
2.4 分层架构模式:让每一层各司其职
分层架构,是嵌入式系统最经典的架构模式之一。我习惯把超声系统分成四层:
- 硬件抽象层(HAL):封装硬件寄存器操作,提供统一的硬件接口。
- 驱动层:管理具体外设,如探头、ADC、FPGA等。
- 业务逻辑层:实现超声核心算法,如波束合成、图像处理、测量计算。
- 应用层:处理用户交互、界面显示、数据存储。
每一层只依赖它的下一层,绝不跨层调用。比如,业务逻辑层只能调用驱动层的接口,不能直接操作硬件寄存器。应用层只能调用业务逻辑层的接口,不能直接调用驱动层。
这样做的好处是什么?每一层都可以独立替换、独立测试。你想换一个探头?只需要改驱动层,业务逻辑层完全不用动。你想把图像处理算法从CPU迁移到GPU?只需要改业务逻辑层的实现,应用层和驱动层都不受影响。
我记得有一次,客户要求支持一个新的探头型号。我只需要在驱动层加一个新的探头驱动,然后配置一下参数。业务逻辑层和应用层一行代码都没改。这就是分层架构的威力。
分层架构的黄金法则:
- 上层可以调用下层,下层绝不能调用上层。
- 同层之间可以互相调用,但尽量通过接口解耦。
- 每一层只暴露必要的接口,隐藏内部实现细节。
当然,分层也不是万能的。层数太多,调用链路过长,性能会受影响。超声系统对实时性要求很高,所以我在实际项目中,会把一些性能敏感的路径做「层间直通」——比如波束合成模块直接访问DMA缓冲区,跳过驱动层的封装。但这是特例,不是常规做法。
2.5 避坑指南:我踩过的几个坑
讲了这么多原则,最后分享几个我亲身踩过的坑,希望能帮你少走弯路。
- 坑一:过度设计。 我曾经为了追求「完美模块化」,把一个简单的功能拆成了七八个类,结果代码可读性极差,维护成本反而更高。记住,模块化是为了解决问题,不是为了炫技。
- 坑二:接口膨胀。 一个模块的接口,随着需求增加越来越多。如果不加控制,最后会变成一个「万能接口」。我建议每个迭代结束后,专门花时间清理接口,去掉那些不再使用的。
- 坑三:分层混乱。 有人为了图方便,在应用层直接调用硬件抽象层。短期看是省事了,长期看就是灾难。一旦硬件换了,所有依赖它的代码都要改。我曾经在一个项目里花了整整两周来重构这种「跨层调用」的代码。
好了,这一章的内容就到这里。下一章我会讲具体怎么用这些原则来设计超声系统的软件架构,包括模块划分、接口定义、层间通信等实战内容。咱们下章见。