SOLID Design in Practice: Plug-and-Play Gimbal Expansion with gimbal-sdk
When the amount of code increases from hundreds to tens of thousands of lines, a small change may cause a chain of red errors; I want to add new functions, but find that the old logic is difficult to start with; when handing over the code, colleagues need to trace it back repeatedly to understand it. In fact, these are all manifestations of failure to implement design principles. This article is intended for you who have a certain C++ foundation but lack system engineering experience.gimbal-sdk For example, I will use visible code and reproducible experiments to quickly understand the five design principles of SOLID, and intuitively feel the immediate benefits it can bring to the project.
01 What is SOLID?
SOLID is an acronym for five words and is a core design concept that every developer who writes complex software must master:

S(Single Responsibility Principle)-Single Responsibility Principle: Each class/module only does one thing.
O(Open/Closed Principle)-Opening and closing principle: New functions can be expanded, but old code should not be touched.
L(Liskov Substitution Principle)-Richter Substitution Principle: Subclasses can safely replace parent classes anywhere.
I(Interface Segregation Principle)-Interface isolation principle: The interface should be small, dedicated, and not bloated.
D(Dependency Inversion Principle)- Dependency inversion principle: The upper-layer code should not rely on the underlying implementation, but on abstraction.
02 Project practice
gimbal-sdk
When working on robot projects, we often encounter this problem: we need to connect to a variety of PTZs (camera or laser equipment that can control rotation), and each PTZ has different protocols and communication methods. If the support is directly "hard-coded", the result is that each time a model is added, the code must be changed significantly. When the communication method is changed (such as changing from serial port to TCP/IP), the system must be started over again. The system becomes more and more bloated and difficult to maintain. AMOVLAB's gimbal-sdk does the opposite. By following SOLID design principles, it allows you to easily expand new models, switch communication methods, and even test each component independently. Adding models, changing protocols, and doing tests all become orderly.

Single Responsibility Principle (SRP)
Application points
Each module should focus on one responsibility to avoid functional clutter.
Project practice
1. IOStreamBase class: It is only responsible for "data stream sending and receiving" and does not care about any protocol packages or control logic.
2. amovGimbalBase and its subclasses: only care about "how to control the PTZ", not what ports are used at the bottom.
Code example
// Communication only
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;
…
};
// Gimbal control only
class IamovGimbalBase {
public:
virtual int setGimabalPos (const AMOV_GIMBAL_POS_T &pos);
…
};
Advantages: The code is clear. If you encounter a bug, you only need to check one module. Modifying a certain function will not affect the entire code.
Open-Closed Principle (OCP)
Application points
Support function expansion through inheritance and composition to avoid modifying existing code.
Project practice
1. When adding a new gimbal model, inherit amovGimbalBase and register the factory.
2. When changing the communication protocol, implement a new IOStreamBase subclass.
Code example
class g1GimbalDriver : protected amovGimbalBase {
static amovGimbal::amovGimbalBase
*creat(amovGimbal::IOStreamBase *_IO)
{
return new g1GimbalDriver(_IO);
}
};
// Register the factory function in the list
callbackMap amovGimbals =
{{"G1", g1GimbalDriver::creat}};
// New model: no changes needed in the main workflow
auto* gimbal = new amovGimbal::gimbal("G1", io);
Advantages: It is easy to expand, reduces the risk of modification, and the system structure is stable.
Richter Substitution Principle (LSP)
Application points
When using base class pointers or references, there is no need to care about specific subclasses, and program logic is not affected.
Project practice
All gimbals inherit amovGimbalBase, the main process gets a base class pointer, which can be operated uniformly regardless of the specific model (G1/G2/AT10, etc.).
Code example
auto* gimbal = new amovGimbal::gimbal("G1", io);
gimbal->setAngle(0, 0, 0); // Works with G1, G2, and AT10
Advantages: Realize true polymorphism, unified logic, and high code reusability.
Interface Segregation Principle (ISP)
Application points Interfaces should be concise and specific, avoiding unnecessary methods.Project practice 1. IOStreamBase only writes the core reading and writing methods of communication, and does not throw in "logs and tests" together. 2. C and C++ have their own interfaces, and the caller can choose according to needs.Code example
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;
// No unrelated methods
};
Advantages: The interface is easy to use, the functions are focused, and redundancy is avoided.
Dependency Inversion Principle (DIP)
Application points
High-level modules should rely on interfaces or abstract classes to avoid direct dependence on underlying implementations.
Project practice
1. The PTZ control module only relies on IOStreamBase and does not need to pay attention to communication details.
2. MockStream can be injected to replace real communication during testing.
Code example
class MockStream : public IOStreamBase { /* Mock communication for testing */ };
amovGimbalBase* gimbal = new G1GimbalDriver(new MockStream());
Advantages: Support module replacement and unit testing to enhance system flexibility.
gimbal-sdk design highlights:
-
Interface priority: All modules rely on abstract interfaces and are completely free from binding to specific hardware and protocols.
-
The division of labor is clear: The factory is responsible for the creation, the driver is only responsible for the movement of the gimbal, the IO is focused on data transmission and reception, and each model does its own thing.
-
Clear hierarchy and strong scalability: The structure is easy to understand, and local modifications can take effect. It is a hands-on example for learning SOLID and layered architecture.
03 How Beginners Learn and Practice
Read from the interface
First read through the core abstractions such as GimbalBase and IOStreamBase to clarify the responsibilities and dependency boundaries of each layer.
write an extension
Add a new virtual PTZ or MockStream to verify the plug-in effect of "access without changing the main process".
Make a counterexample
For example, deliberately write IO and driver in the same category, and then try to add new models to compare maintenance and testing costs.
Continuous review
Every time you read or write a piece of code, pay attention to whether it only does one thing? If we want to add functions, can we not touch the old logic?
Resource Express
The source code has been made available through the SpireCV project.
Source code link:https://gitee.com/amovlab/SpireCV/tree/master/gimbal_ctrl
If you have any questions, or want to learn more about a code example of a certain principle, please leave a message or communicate!
