SOLID设计实践,gimbal-sdk“插拔式”云台扩展

当代码量从几百行涨到几万行,一个微小改动就可能引发连锁红错;想加新功能,却发现旧逻辑难以下手;代码交接时,同事需要反复追溯才能看明白。其实这些都是设计原则没落地的表现。本文面向有一定C++基础、但缺乏系统工程经验的你,以阿木实验室gimbal-sdk为例,带你用看得见的代码、可复现的实验,快速理解SOLID五大设计原则,并直观感受它能给项目带来哪些立竿见影的好处。

01 什么是SOLID?

SOLID是五个单词的首字母缩写,是每一个写复杂软件的开发者必须掌握的核心设计理念:

S(Single Responsibility Principle)-单一职责原则:每个类/模块只做好一件事。
O(Open/Closed Principle)-开闭原则:能扩展新功能,但不要动老代码。
L(Liskov Substitution Principle)-里氏替换原则:子类能在任何地方安全地替换父类。
I(Interface Segregation Principle)-接口隔离原则:接口要小,要专一,不要臃肿。
D(Dependency Inversion Principle)-依赖倒置原则:上层代码不要依赖底层实现,而要依赖抽象。

02 项目实践

gimbal-sdk

在做机器人项目时,常会遇到这样的问题:要接入多种云台(可控制转动的摄像头或激光设备),每种云台的协议、通信方式都不同。如果直接“硬编码”支持,结果往往是每加一个型号就得大改代码,通信方式一换(比如从串口改成TCP/IP)又得推倒重来,系统越做越臃肿,难以维护。阿木实验室的gimbal-sdk则是反其道而行之。它通过遵循 SOLID设计原则,让你可以轻松扩展新型号、切换通信方式,甚至独立测试每个组件。加型号、换协议、做测试,全都变得有章可循。

单一职责原则(SRP)

应用要点
每个模块应该专注于一项职责,避免功能混乱。
项目实践
1. IOStreamBase类:只负责“数据流收发”,什么协议包、控制逻辑一概不管。
2. amovGimbalBase及其子类:只关心“怎么控制云台”,不关心底层用什么端口。
代码举例

// 只做通信
class IOStreamBase {
public:
    virtual size_t write(const uint8_t *buf, size_t len) = 0;
    virtual size_t read(uint8_t *buf, size_t len) = 0;
…
};
// 只做云台控制
class IamovGimbalBase {
public:
    virtual int setGimabalPos (const AMOV_GIMBAL_POS_T &pos);
…
};

优势:代码清晰,遇到bug只需查一个模块,修改某个功能不会牵动全部代码。

开闭原则(OCP)

应用要点
通过继承和组合支持功能扩展,避免修改已有代码。
项目实践
1. 新增云台型号时,继承amovGimbalBase,注册工厂即可。
2. 更换通信协议时,实现新的IOStreamBase 子类。

代码举例

class g1GimbalDriver : protected amovGimbalBase { 
    static amovGimbal::amovGimbalBase
*creat(amovGimbal::IOStreamBase *_IO)
    {
        return new g1GimbalDriver(_IO);
    }
};
// 在列表中注册创建函数
callbackMap amovGimbals =

{{"G1", g1GimbalDriver::creat}};
// 新型号,主流程不用动
auto* gimbal = new amovGimbal::gimbal("G1", io);

优势:易于扩展,减少改动风险,系统结构稳定。

里氏替换原则(LSP)

应用要点
使用基类指针或引用时,无需关心具体子类,程序逻辑不受影响。
项目实践
所有云台都继承amovGimbalBase,主流程拿到一个基类指针,无论具体型号(G1/G2/AT10等),都能统一操作。

代码举例

auto* gimbal = new amovGimbal::gimbal("G1", io);
gimbal->setAngle(0, 0, 0); //G1、G2、AT10都能用

优势:实现真正的多态,逻辑统一,代码复用性高。

接口隔离原则(ISP)

应用要点接口应精简、专一,避免包含不必要的方法。项目实践1. IOStreamBase只写通信最核心的读写方法,不把什么“日志、测试”一起丢进去。2. C和C++分别有各自接口,调用者按需选择。代码举例

class IOStreamBase {
    virtual size_t write(const uint8_t *buf, size_t len) = 0;
    virtual size_t read(uint8_t *buf, size_t len) = 0;
    // 没有乱七八糟的方法
};

优势:接口易用,功能专注,避免冗余。

依赖倒置原则(DIP)

应用要点
高层模块应依赖接口或抽象类,避免直接依赖底层实现。
项目实践
1. 云台控制模块只依赖 IOStreamBase,无须关注通信细节。
2. 测试时可注入 MockStream 替代真实通信。

代码举例

class MockStream : public IOStreamBase { /* 测试用的假通信 */ };
amovGimbalBase* gimbal = new G1GimbalDriver(new MockStream());

优势:支持模块替换与单元测试,增强系统灵活性。

gimbal-sdk设计亮点:

  • 接口优先:所有模块都依赖抽象接口,彻底摆脱对具体硬件和协议的绑定。

  • 分工明确:工厂负责创建,驱动只管云台动作,IO 专注数据收发,各型号各做其事。

  • 层次清晰,可扩展性强:结构易懂、改动局部即可生效,是学习 SOLID 和分层架构的上手范例。

03 入门者如何学习和实践

从接口读起

先通读GimbalBase、IOStreamBase 等核心抽象,弄清各层职责与依赖边界。

写一次扩展

新增一个虚拟云台或MockStream,验证“不改主流程即可接入”的插拔式效果。

做一次反例

比如故意把IO与驱动写在同一类,再尝试新增型号,对比维护与测试成本。

持续复盘

每读或写完一段代码,都要注意它是否只做一件事?若要加功能,能否不触及旧逻辑?

资源速递
源码已通过SpireCV项目开放。
源码链接:https://gitee.com/amovlab/SpireCV/tree/master/gimbal_ctrl
如有任何问题,或想深入了解某一原则的代码案例,欢迎留言或交流!

如果您有感兴趣的技术话题,请在留言区告诉我们!关注阿木实验室,更多技术干货不断更新!
开发遇到棘手难题可以上阿木官方论坛:bbs.amovlab.com
有工程师亲自解答
10000+无人机开发者和你共同进步!