模块化设计三原则:高内聚低耦合、单一职责、接口隔离

好,我们接着聊。上一章我们讲了模块划分的宏观思路,这一章咱们深入一点,聊聊模块化设计的三个核心原则。说白了,这三个原则就是衡量你代码写得好不好的三把尺子。

我个人习惯,在评审代码时,脑子里会不自觉地用这三条原则去套。如果哪条没满足,我就会打个问号——这里是不是可以重构一下?

高内聚低耦合:模块的“内功”与“外功”

先说说高内聚低耦合。这是模块化设计里最基础、也最重要的一条。

高内聚,指的是一个模块内部的所有元素(函数、变量、数据结构)都紧密相关,共同完成一个明确的任务。说白了,就是“一家人不说两家话”。

低耦合,指的是模块与模块之间的依赖关系要尽可能弱。一个模块改了,其他模块尽量不受影响。说白了,就是“各扫门前雪”。

在剃须刀里,我举个例子。电机控制模块,它内部应该只做一件事:根据指令控制电机转速和转向。它不应该关心电池电量是多少,也不应该知道按键被按了几次。这就是高内聚。

那低耦合呢?电机控制模块通过一个简单的接口对外提供服务,比如 motor_set_speed(uint8_t speed)。其他模块想控制电机,调用这个接口就行,不需要知道电机是PWM控制的还是PID控制的。这就是低耦合。

核心观点: 高内聚让模块内部“拧成一股绳”,低耦合让模块之间“井水不犯河水”。

我在项目中遇到过一个问题。有个同事把电机控制和LED指示逻辑写在了一起。电机转的时候,LED也跟着闪。后来客户要求改LED闪烁模式,结果电机控制也出了问题。你看,这就是内聚不够、耦合太紧的典型后果。

单一职责原则:一个模块只做一件事

单一职责原则,英文叫Single Responsibility Principle,简称SRP。它是高内聚的进一步细化。

这个原则说:一个模块(或者一个函数、一个类)应该只有一个引起它变化的原因。换句话说,一个模块只负责一个功能点。

你想想看,如果一个模块既负责电机控制,又负责电池管理,还负责按键扫描。那将来任何一个需求变了,你都得改这个模块。改来改去,迟早改出bug来。

在剃须刀嵌入式软件里,我建议这样划分职责:

模块 单一职责 对外接口示例
电机控制模块 控制电机启停、转速、转向 motor_start(), motor_set_speed()
电池管理模块 监测电量、充电管理、低电报警 battery_get_level(), battery_is_charging()
按键扫描模块 检测按键按下、释放、长按 key_get_event()
LED指示模块 控制LED亮灭、闪烁模式 led_set_mode()

每个模块只做一件事,而且把这件事做好。这就是单一职责原则的精髓。

我的小技巧: 写代码前,先问自己一个问题:“这个模块将来会因为什么原因而修改?”如果答案超过一个,那它可能违反了单一职责原则。

接口隔离原则:不要强迫别人依赖他们不需要的东西

接口隔离原则,英文是Interface Segregation Principle,简称ISP。这个原则说的是:客户端不应该被迫依赖它们不使用的接口。

嗯,这句话有点绕。我换个说法:你提供的接口,应该尽量小、尽量专一。不要搞一个大而全的接口,让调用者去“消化”一堆用不上的东西。

举个例子。假设我们有一个“电源管理”接口,里面包含了电池电量读取、充电控制、电机供电开关、LED供电开关。但电机控制模块只需要“电机供电开关”这一个功能。如果它依赖了整个电源管理接口,那它就被迫知道了电池电量、充电控制这些它不需要的东西。

这有什么问题呢?问题大了。将来电源管理接口改了,比如充电控制的参数变了,电机控制模块也得重新编译、重新测试。明明它只用了其中一个函数,却要承担整个接口变更的风险。

正确的做法是:把接口拆小。比如:

// 不好的做法:大而全的接口
typedef struct {
    uint8_t (*get_battery_level)(void);
    void (*start_charging)(void);
    void (*stop_charging)(void);
    void (*motor_power_on)(void);
    void (*motor_power_off)(void);
    void (*led_power_on)(void);
    void (*led_power_off)(void);
} power_manager_t;

// 好的做法:拆分成小接口
typedef struct {
    void (*power_on)(void);
    void (*power_off)(void);
} motor_power_interface_t;

typedef struct {
    uint8_t (*get_level)(void);
    void (*start_charge)(void);
    void (*stop_charge)(void);
} battery_interface_t;

你看,电机控制模块只需要依赖 motor_power_interface_t,电池管理模块只需要依赖 battery_interface_t。各取所需,互不干扰。

避坑指南: 我曾经在一个项目里,为了图省事,把所有外设驱动都塞进了一个大结构体里。结果每次改一个驱动,所有依赖这个结构体的模块都要重新编译。那叫一个痛苦。后来我花了整整两天时间重构,把接口拆分开,编译时间从5分钟降到了30秒。嗯,这个教训我记一辈子。

三个原则如何协同工作

这三个原则不是孤立的,它们相辅相成。

  • 单一职责原则帮你确定“模块应该做什么”。
  • 接口隔离原则帮你确定“模块应该暴露什么”。
  • 高内聚低耦合是最终的目标状态。

你想想看,如果每个模块都遵循单一职责,那它的内聚性自然就高。如果每个接口都遵循接口隔离,那模块之间的耦合自然就低。这三个原则,其实是从不同角度在说同一件事:让代码更清晰、更易维护、更可复用。

在剃须刀项目里,我建议你从这三个原则出发,重新审视一下你的代码。看看有没有哪个模块“身兼数职”?有没有哪个接口“大而全”?如果有,别犹豫,重构它。

记住一句话:好的代码不是写出来的,是改出来的。而这三个原则,就是你改代码时的指路明灯。