SOLID原则:面试官问“你怎么设计类的“,我用五个字母回答了他
前面聊了不少设计模式——工厂、单例、观察者、策略、状态模式。但设计模式是术SOLID原则才是道。面试官有时候会问一个很开放的问题你是怎么设计类的这个问题没有标准答案但如果你能提到SOLID五个原则再结合项目里的例子讲清楚面试官基本会点头。因为这五个字母背后是几十年软件工程实践总结出来的面向对象设计的基本功。S——单一职责原则一个类只应该有一个引起它变化的原因。听着很抽象翻译成人话就是一个类别干太多事。之前在项目里见过一个SensorManager类干了什么呢读取传感器数据、做数据滤波、写日志、发ROS2话题、还要处理串口通信。五个功能塞在一个类里改任何一个功能都可能影响其他功能。// 反面教材一个类干了太多事 class SensorManager { void readData(); // 读传感器 void filterData(); // 滤波 void writeLog(); // 写日志 void publishTopic(); // 发话题 void handleSerial(); // 串口通信 };拆开来看就舒服多了class SensorReader { void read(); }; class DataFilter { void filter(); }; class DataPublisher { void publish(); }; class Logger { void log(); };每个类只管一件事。改滤波算法不影响读取逻辑改日志格式不影响发布话题。O——开闭原则对扩展开放对修改关闭。前面讲的设计模式工厂模式、策略模式、状态模式本质上都在实现开闭原则——加新功能的时候加代码别改老代码。举个机器人的例子。你的机器人要支持多种传感器最初只有激光雷达和IMU。你写了个if-elseif (type lidar) { initLidar(); } else if (type imu) { initIMU(); }后来要加相机、超声波、GPS……每次加新传感器都要改这段代码。用开闭原则的做法是定义一个Sensor接口每种传感器实现自己的初始化逻辑。加新传感器只需要加一个新类不用动已有的代码。L——里氏替换原则子类应该能替换父类而不影响程序的正确性。这个原则说白了就是继承关系要合理。子类不能偷偷改变父类的行为。经典反面案例正方形继承矩形。矩形的setWidth和setHeight正方形继承之后重写了setWidth——setWidth同时改了宽和高。结果呢用矩形的代码去操作正方形行为就错了。在机器人开发里这个坑也常见。比如你有一个BaseController有个move(double speed)方法。子类FourWheelController正常实现但子类HoverController重写了move——它要求speed必须大于某个阈值才能悬浮。传一个小的speed进去HoverController直接忽略了。这就是违反里氏替换。class BaseController { public: virtual void move(double speed) 0; }; // 违反里氏替换子类偷偷改变了move的语义 class HoverController : public BaseController { public: void move(double speed) override { if (speed 0.5) return; // 静默忽略调用者不知道 // 真正的移动逻辑 } };正确做法是什么如果HoverController有特殊要求应该在接口层面就表达出来而不是偷偷加限制。I——接口隔离原则客户端不应该被迫依赖它不使用的接口。翻译一下接口别太大。如果你的接口有20个方法但某个实现类只需要其中3个那这个接口就该拆。// 反面一个巨大的接口 class RobotInterface { virtual void move() 0; virtual void sense() 0; virtual void plan() 0; virtual void communicate() 0; virtual void selfDiagnose() 0; }; // 拆分后每个接口职责明确 class Movable { virtual void move() 0; }; class Sensable { virtual void sense() 0; }; class Plannable { virtual void plan() 0; };这样一个只需要移动功能的模块只要实现Movable就行了不用被迫实现一堆用不到的方法。D——依赖倒置原则高层模块不应该依赖低层模块两者都应该依赖抽象。这条原则是解耦的核心。你的导航模块不应该直接依赖具体的激光雷达驱动而应该依赖一个抽象的传感器接口。// 反面高层直接依赖低层具体实现 class Navigator { RPLidarA1 lidar_; // 绑死了具体型号 void navigate() { auto scan lidar_.getScan(); // ... } }; // 正面依赖抽象接口 class Navigator { LidarInterface lidar_; // 依赖抽象 void navigate() { auto scan lidar_.getScan(); // ... } };这样换激光雷达型号的时候Navigator一行代码都不用改。写个新的RPLidarA1的驱动类实现LidarInterface接口就行。五个原则在面试中的实战面试官问你怎么设计类的不是让你背概念。他想听的是你怎么把这五个原则用到实际项目里的。我的经验是这么组织回答先说我会用单一职责来拆分模块每个类只管一件事。然后说接口设计要遵循接口隔离原则不要把接口搞得太大太臃肿。接着提模块之间尽量通过抽象接口来交互不要直接依赖具体实现。如果面试官继续追问再展开讲开闭原则和里氏替换原则。关键是结合项目实例来说明。比如你可以说在我的SLAM项目里前端里程计和后端优化之间通过抽象接口通信这样换前端算法不影响后端。这比背十遍SOLID的定义都管用得多。给正在准备面试的你SOLID原则是面向对象设计的底线要求。面试里不一定会直接问你SOLID是什么但会通过各种问题间接考察——你怎么设计这个模块你这个类为什么这么拆如果换一种传感器你的代码要怎么改。前面讲的设计模式每一个都在实践SOLID的某个原则。工厂模式实现开闭原则策略模式实现依赖倒置单一职责原则更是几乎所有模式的前提。理解了SOLID设计模式就不再是死记硬背的套路而是自然而然的设计选择。分享一个面试经验面试时被问到系统设计类问题可以先用SOLID原则分析现有方案的不足再提出改进方案。比如原来的设计违反了单一职责原则一个类管太多了我建议拆成三个类各自负责不同的功能。这种有分析过程的回答比直接给答案更能体现设计能力面试官看重的不只是结果更是你的思考过程和表达能力。再补充一个面试中经常被追问的话题里氏替换原则LSP在实际项目中怎么验证。里氏替换原则说的是子类必须能替换父类而不破坏程序的正确性。在机器人开发里如果你有一个Sensor基类和一个Lidar子类那所有使用Sensor的地方换成Lidar都应该正常工作。验证LSP的一个实用方法是写单元测试——用父类接口写一套测试用例然后让所有子类都跑一遍。如果某个子类跑不过说明它违反了里氏替换原则需要重新审视继承关系。面试时如果你能提到用单元测试来验证LSP面试官会觉得你不只是理解理论还有工程化的验证手段。另外在实际项目中违反LSP最常见的表现就是子类重写了父类方法但改变了语义——比如父类的move是向前移动子类重写后变成了旋转这种设计上的不一致在面试中是很好的反面教材。下篇聊C内存模型——栈、堆、全局区、代码区的分布。这个知识点是理解变量生命周期和内存管理的基础面试里经常和指针、引用一起考。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第48篇 设计模式之状态模式——机器人行为管理的优雅方案 下一篇预告第50篇 C内存模型详解——栈/堆/全局区/代码区的分布有任何问题欢迎评论区留言我会尽量回复。