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-else:
if (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++内存模型详解——栈/堆/全局区/代码区的分布
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)