嵌入式 C 转 嵌入式Linux番外篇(01):拒绝“屎山”代码!五大SOLID原则,教你写出优雅的C++
大家好,我是一点一电 👋。
很多从嵌入式C语言转战C++的兄弟都有一个困惑:
“C++的类(Class)不就是C语言里的结构体(Struct)加几个函数指针吗?为什么要搞得那么复杂?”
刚开始写C++时,我们往往会把所有功能塞进一个Class里,或者用大量的 switch-case 来处理逻辑。结果就是:需求一改,代码全改;改了一个Bug,引出十个Bug。
其实,面向对象(OOP)真正的威力不在于语法,而在于设计思想。今天我们就来聊聊面向对象设计的五大金科玉律——SOLID原则。
我们会用只有C语言基础也能看懂的简单C++代码,带你领略设计的魅力!
1. S - 单一职责原则 (SRP)
Single Responsibility Principle
一句话解释: 一个类,应该只负责做一件事情。
❌ C语言思维的陷阱: 在写C语言时,我们习惯在 main.c 或者一个大的 app.c 里写完初始化、逻辑控制、日志打印。 到了C++,如果你写了一个 SystemManager 类,既负责连接WiFi,又负责解析JSON,还负责存数据库,那它就太累了。
错误示范:
classUserSettings {public:voidchangePassword(constchar* newPwd){// ... 修改密码逻辑// 坏味道:这里竟然还负责把日志写到文件里?// 如果以后要改成写到数据库,这个类就得改代码! FILE* f = fopen("log.txt", "a"); fprintf(f, "User changed password\n");fclose(f); }};
✅ SOLID 改进: 把“改密码”和“写日志”拆开。
// 1. 专门负责记录日志的类classLogger {public:voidlog(constchar* message){// 实际开发中可以使用 std::cout 类似于 printfprintf("[Log]: %s\n", message); }};// 2. 专门负责用户设置的类classUserSettings {private: Logger* myLogger; // 持有一个日志工具的指针(类似于C的结构体指针)public:// 构造函数:初始化时传入日志工具UserSettings(Logger* logger) { myLogger = logger; }voidchangePassword(constchar* newPwd){// ... 修改密码的核心逻辑// 委托 Logger 去干它该干的事 myLogger->log("User changed password"); }};
收益: 如果以后日志需要发给云端,你只需要修改 Logger 类,UserSettings 类完全不用动!
2. O - 开闭原则 (OCP)
Open/Closed Principle
一句话解释: 对扩展开放,对修改关闭。
这是SOLID中最重要的一条! 对扩展开放:想要增加新功能(比如支持新传感器)?没问题,加个新文件(新类)就行。 对修改关闭:增加新功能时,不要去改动原有的、已经测试通过的代码。
❌ C语言思维的陷阱: 遇到不同类型,最爱用 if-else 或 switch。
// 假设我们要画不同的图形voiddraw(int shapeType){if (shapeType == 1) {drawCircle(); } elseif (shapeType == 2) {drawRectangle(); }// 💀 如果老板让你再加一个三角形,你就要来改这行代码!// 每次改动都可能把原来的逻辑搞挂。}
✅ SOLID 改进: 利用C++的 多态(Polymorphism)。在C语言里,这相当于结构体里放了一个通用的函数指针。
// 1. 定义一个抽象基类(相当于定义一个接口规范)classShape {public:// virtual 关键字:告诉编译器,这个函数是可以被子类重写的// = 0 表示这是纯虚函数,子类必须实现它virtualvoiddraw()= 0; };// 2. 圆形(扩展很容易,新建一个类就行)classCircle : public Shape {public:voiddraw()override{printf("Drawing a Circle\n"); }};// 3. 矩形classRectangle : public Shape {public:voiddraw()override{printf("Drawing a Rectangle\n"); }};// 4. 绘图模块(核心逻辑)// 注意:参数是 Shape 指针(父类指针),它可以指向圆形,也可以指向矩形voidstartDrawing(Shape* shape){// 这一行代码不需要改动,无论你传进来的是圆形还是三角形 shape->draw(); }
** 收益:** 如果明天要加“三角形”,你只需要新建一个 Triangle 类继承 Shape,不需要去动 startDrawing 函数里的任何一个字符!
3. L - 里氏替换原则 (LSP)
Liskov Substitution Principle
一句话解释: 子类必须能完全替代父类,否则就不要继承。
这就好比:如果你宣称这是个“鸭子”类,那它必须能游泳。如果你搞了个“ 玩具鸭”继承“鸭子”,结果调用 swim() 时它沉底了(报错或行为异常),那就违反了LSP。
❌ 错误示范: 我们常说“正方形是特殊的长方形”,但在代码里这样继承是错的。
classRectangle {public:virtualvoidsetWidth(int w){ width = w; }virtualvoidsetHeight(int h){ height = h; }protected:int width, height;};// 正方形继承长方形classSquare : public Rectangle {public:// 正方形的长宽必须相等,所以改一个,另一个也要变voidsetWidth(int w)override{ width = w; height = w; }voidsetHeight(int h)override{ height = h; width = h; }};// 一个测试函数voidprocess(Rectangle& r){ r.setWidth(5); r.setHeight(10);// 按照长方形逻辑,面积应该是 5 * 10 = 50// 但如果是正方形传进来,面积变成了 10 * 10 = 100!// 这里的逻辑崩坏了!printf("Area: %d\n", r.width * r.height); }
收益: 继承虽然好用,但不要为了复用代码而强行继承。如果行为不一致,就不要用继承关系。
04. I - 接口隔离原则 (ISP)
Interface Segregation Principle
一句话解释: 不要给别人他不需要的东西。接口要尽量小,尽量专一。
❌ C语言思维的陷阱: 定义了一个超级大的结构体或接口,里面包罗万象。
// 一个超级全能的工人类classSuperWorker {public:virtualvoidcode()= 0; // 写代码virtualvoideat()= 0; // 吃饭};// 程序员类:没问题,会写代码也会吃饭classProgrammer : public SuperWorker {public:voidcode(){ printf("Coding...\n"); }voideat(){ printf("Eating lunch...\n"); }};// 机器人推土机:有问题!classRobot : public SuperWorker {public:voidcode(){ printf("Processing logic...\n"); }// 机器人不需要吃饭,但因为继承了接口,被迫实现这个函数voideat(){ // 这里的实现很尴尬,甚至可能报错printf("Error: Robots don't eat!\n"); }};
✅ SOLID 改进: 拆分接口!
classWorkable {public:virtualvoidwork()= 0;};classEatable {public:virtualvoideat()= 0;};// 程序员:即工作,又吃饭classProgrammer : public Workable, public Eatable {public:voidwork(){ printf("Coding...\n"); }voideat(){ printf("Yummy!\n"); }};// 机器人:只工作classRobot : public Workable {public:voidwork(){ printf("Beep Boop Working...\n"); }// 根本不需要实现 eat(),清爽!};
5. D - 依赖倒置原则 (DIP)
Dependency Inversion Principle
一句话解释: 哪怕是高层模块(比如主业务逻辑),也不要直接依赖底层模块(比如具体的某个传感器),两者都应该依赖抽象(接口)。
这就像你家里的台灯插头(高层)不应该直接焊死在墙里的电线(底层)上,而是大家都依赖标准的“三孔插座”(接口)。
❌ 错误示范:
classKeyboard {// ... 具体键盘实现};classComputer {private: Keyboard k; // ❌ 电脑直接依赖了具体的“键盘类”// 如果我想换个蓝牙键盘?换个游戏键盘?// 必须修改 Computer 类的代码!耦合度太高。};
✅ SOLID 改进:
// 1. 定义一个通用的输入设备接口classIInputDevice {public:virtualvoidinput()= 0;};// 2. 具体的键盘实现这个接口classKeyboard : public IInputDevice {public:voidinput()override{ printf("Typing...\n"); }};// 3. 电脑依赖的是接口(IInputDevice),而不是具体的键盘classComputer {private: IInputDevice* inputDev; // 这里存的是接口的指针public:// 依赖注入:在电脑启动时,把设备插进去voidsetInputDevice(IInputDevice* dev){ inputDev = dev; }voidwork(){ inputDev->input(); }};
收益: 以后无论你出蓝牙键盘、语音输入、甚至脑机接口,只要它们实现了 IInputDevice,Computer 类一行代码都不用改就能直接用!
总结
C++ 是一把双刃剑。
- • 乱用 Class,你会得到比 C 语言更难维护的“面条代码”。
- • 善用 SOLID,你会发现代码变得像乐高积木一样,模块清晰,随意插拔。
作为C语言转过来的开发者,最难的不是学语法,而是转变思维。
哪怕你现在还不能完全理解,先把这五个原则背下来,下次写 class 的时候问问自己:“我这样写,以后好改吗?”
觉得有收获的话,点个赞再走吧!👍
注:本文代码仅为示例,为了方便C语言开发者理解,简化了内存管理(如智能指针)等细节。