ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

第312篇 模块化设计——高内聚低耦合的实践

第312篇 模块化设计——高内聚低耦合的实践 上篇聊了软件架构的整体设计。这篇聚焦到一个更具体的话题模块化设计。怎么把一个大系统拆分成合理的模块模块之间的接口怎么设计这是每个机器人软件工程师每天都要面对的问题。模块化设计的好坏直接影响开发效率、代码质量和系统可维护性。面试中面试官经常通过你的模块化设计能力来判断你的工程水平。什么是好的模块化好的模块化有两个核心特征高内聚、低耦合。高内聚指的是一个模块内部的元素紧密相关。一个负责路径规划的模块里面所有的类和函数都应该跟路径规划有关。如果你发现一个模块里既有路径规划又有传感器驱动的代码那就是内聚性差的表现。低耦合指的是模块之间的依赖关系尽量少、尽量简单。两个模块之间如果只通过一个简单的数据结构通信耦合度很低。如果它们互相调用对方的内部函数、共享全局变量、依赖对方的执行时序耦合度就很高。低耦合的直接好处是修改一个模块时不需要改动其他模块。这在多人协作的项目中尤其重要——你改你的路径规划算法不需要担心影响别人的感知模块。SOLID原则是模块化设计的理论基础。单一职责原则SRP一个模块只做一件事。开闭原则OCP对扩展开放对修改关闭——新增功能通过添加新代码而不是修改旧代码。里氏替换原则LSP子类可以替换父类而不影响正确性。接口隔离原则ISP不要强迫使用者依赖它们不需要的接口。依赖反转原则DIP高层模块不依赖低层模块两者都依赖抽象。这五个原则在面试中经常被问到要能结合项目经验来解释。接口设计的原则模块之间的接口是模块化设计中最关键的部分。接口设计得好模块可以独立开发、独立测试、独立替换。接口设计得差改一个地方到处都要改。接口设计的几个原则。最小知识原则。模块只暴露它必须暴露的东西其余全部隐藏。C中用private和public来控制Python中用下划线前缀表示私有。接口越简洁使用者的认知负担越低出错的可能性越小。面向接口编程。模块之间通过抽象接口abstract class或interface通信而不是具体实现。这样你可以随时替换实现——比如把A*路径规划换成RRT只要它们实现了同一个接口调用方不需要改动。数据契约。模块之间传递的数据结构要有明确的定义通常用protobuf、flatbuffers或者简单的struct。数据结构的变更要向后兼容——新增字段可以删除或修改已有字段要谨慎。// 好的接口设计示例 class PathPlanner { public: virtual ~PathPlanner() default; // 统一的接口不暴露内部实现 virtual Path plan(const Pose start, const Pose goal, const OccupancyGrid map) 0; // 查询算法状态 virtual bool isReady() const 0; }; // AStarPlanner和RRTPlanner都实现这个接口 // 调用方只需要知道PathPlanner接口避免使用全局变量来传递模块间的数据。全局变量会让模块之间产生隐式依赖很难追踪和调试。如果确实需要共享大量数据用一个显式的数据管理器模块其他模块通过它来读写数据。接口的版本管理也很重要。当接口需要变更时不要直接修改旧接口而是创建一个新版本。旧接口标记为deprecated给下游模块时间迁移。直接修改接口是最容易导致线上事故的做法——你以为只是改了一个字段名结果下游有五个模块都在用这个字段。机器人中的模块化实践在机器人软件中模块化有一些特定的模式。传感器驱动封装。每种传感器激光雷达、相机、IMU都有一个驱动模块负责跟硬件通信、解析数据。驱动模块向上提供统一的数据接口——不管是什么品牌的激光雷达输出的点云格式都是一样的。这样上层的感知模块不需要关心底层硬件的差异。算法模块的参数化。同一个算法模块比如目标检测可能有多种实现YOLO、CenterPoint、PointPillars。通过配置参数来切换实现而不是写if-else。这样新增一个算法只需要实现同一个接口注册到工厂类中。状态机的模块化。机器人的行为逻辑通常用状态机来管理空闲、导航中、充电中、故障中。每个状态是一个独立的模块状态之间的转换由状态机框架管理。新增一种行为模式就是新增一个状态模块。行为树Behavior Tree是状态机的升级版更适合复杂的决策逻辑。配置驱动的模块化。很多模块的行为需要通过参数来配置比如路径规划器的最大速度、安全距离。把这些参数放到配置文件中YAML或JSON而不是硬编码在代码里。这样改参数不需要重新编译不同机器人可以有不同的配置。ROS的参数服务器就是做这个的。常见的模块化反模式也要避免。上帝模块God Module——一个模块承担了太多职责代码量巨大谁都不敢改。面条式依赖——模块A调B、B调C、C又调A形成循环依赖。隐式接口——模块之间的通信不通过显式的接口定义而是通过全局变量、文件、或者约定俗成的数据格式。这些反模式在快速迭代的项目中很常见但如果不及时治理系统会变得越来越难以维护。面试中的模块化设计题面试官可能给你一个场景让你现场设计模块划分。比如设计一个送餐机器人的软件系统。你要能从功能角度拆分模块感知模块检测障碍物、识别地标、定位模块确定自身位置、导航模块路径规划和跟踪、控制模块电机控制、交互模块语音提示、屏幕显示、调度模块接收订单、分配任务。然后要讲清楚模块之间的接口。感知模块输出障碍物列表给导航模块定位模块输出位姿给导航模块和控制模块导航模块输出目标速度给控制模块。每个接口的数据格式、更新频率、可靠性要求都要说清楚。面试官可能还会追问如果导航模块要换一种算法需要改哪些模块如果你之前的接口设计得好答案是只需要改导航模块内部其他模块不受影响。这就是模块化设计的价值。还可能问你怎么测试一个模块好的模块化设计让单元测试变得容易——你可以用mock对象替代依赖模块单独测试目标模块。比如测试路径规划器时用一张固定的地图数据检查输出的路径是否避开了障碍物、是否满足运动学约束。不需要启动整个系统也不需要真实的传感器数据。模块化设计还有一个好处方便做性能优化。如果你发现系统太慢可以先profile每个模块的耗时找到瓶颈模块集中优化它。如果模块之间耦合度高你连单独测量一个模块的耗时都做不到。关于模块的粒度。太粗一个模块太大失去模块化的好处太细模块太多太小增加了通信开销和管理复杂度。经验法则是一个模块的代码量控制在2000到5000行以内一个人能在两周内完全理解它的逻辑。如果超过了考虑拆分。给你的建议模块化设计是一种实践能力光看书不够要多写代码、多做项目。每次写代码时停下来想一想这个函数放在这个类里合适吗这个模块的职责是不是太多了这个接口是不是太复杂了读优秀的开源项目也是好方法。看看MoveIt2、NAV2这些项目是怎么组织代码的模块之间怎么通信的。然后思考如果让你重新设计你会怎么做面试前准备好你项目中的模块化设计案例。能画出模块图讲清楚每个模块的职责和接口。能解释为什么这样划分以及这种划分带来的好处和代价。代码review是检验模块化设计的好方法。如果你的代码别人一看就懂、改起来不需要看其他模块的代码说明模块化做得好。如果review时经常需要解释这个模块为什么要调用那个模块的内部函数说明接口设计有问题。重构是模块化设计的日常维护。项目初期为了赶进度可能会写一些脏代码——模块职责不清、接口混乱、全局变量满天飞。这些技术债不会自己消失要定期安排重构。每次迭代结束留20%的时间做重构清理那些先这样凑合以后再改的代码。Boy Scout Rule是个好原则每次改代码让代码比你看到时更好一点。推荐读两本书Robert Martin的Clean Architecture和Clean Code。前者讲架构层面的模块化原则依赖反转、组件内聚等后者讲代码层面的模块化实践函数设计、命名规范、错误处理。这两本书的知识在面试中经常被考到。上一篇第311篇 软件架构设计——机器人系统的骨架下一篇预告第313篇 中间件设计——模块之间的桥梁
返回列表