I'm currently working on the control software for a system that includes a lot of user controllable hardware and also has many configurations. Configurations in this context refers to a specific combination of hardware. Each configuration can be thought of as it's own "product" for arguments state.
In previous iterations of the software, hardware configurations were varied at compile time (working in C++) which means we had to generate a lot of installers for each release. Our goals with the new interface are the following:
- Determine hardware connections at run time
- Distribute one installer
In the current design, we're making heavy use of inheritance and dynamic polymorphism. The gist is we have a GUI layer that communicates via view models (MVVM architecture) to the back end (model layer). In the view models, we're using abstract interfaces for each concrete type of hardware in real life (i.e. Camera, Robot and so on). There is also an interface for a Hardware object which means we're also using multiple inheritance.
I've recently been running into talks and posts that show how intrusive inheritance can be but I'm struggling to see what other design we could use to solve our design problem and still address our goals with this software.
As an example, consider the following sample pseudo code (C++). Say we have a configuration that has a camera and a single axis robot.
We'll define the hardware interface as:
class HardwareInterface {
public:
virtual bool connect() = 0;
virtual bool disconnect() = 0;
virtual bool reconnect() = 0;
virtual bool isConnected() = 0;
virtual std::string name() = 0;
virtual std::string serialNumber() = 0;
// ... more
};
class CameraInterfaceCallback {
public:
virtual void newFrameReady(void* data, int width, int height, int depth, int type);
};
class CameraInterface : public CameraInterfaceCallback
{
public:
virtual void setGain(double gain) = 0;
virtual void setFps(double fps) = 0;
};
class RobotInterfaceCallback {
public:
virtual void moveStarted(RobotInterface* robot, int axis);
virtual void moveStopped(RobotInterface* robot, int axis);
};
class RobotInterface : public RobotInterfaceCallback {
public:
virtual void moveTo(double position, int axis) =0;
virtual void moveBy(double move_by, int axis) = 0;
};
class MyCamera : public QObject, public HardwareInterface, public CameraInterface {
// implementation
Q_OBJECT
signals:
void newFrameReady(void* data, int width, int height, int depth, int type);
};
class MyRobot : public QObject, public HardwareInterface, public RobotInterface {
// implementation
Q_OBJECT
signals:
void moveStarted(RobotInterface* robot, int axis);
void moveStopped(RobotInterface* robot, int axis);
};
In the GUI of the project, once the concrete object is created then the object is bound to via it's abstract interface. One other complication is that we use Qt5 so the "Callbacks" are actually implemented as signals and emitted in the concrete hardware class. This way we can have a view that is meant only for the RobotInterface
and just bind the view to the interface and be done with it. This means that if down the line we add MyOtherRobot
, the front end doesn't really care.
My questions are:
- Is there anything intrinsically wrong with our approach (which feels like abuse of inheritance)?
- Is there any advantage to using actual callbacks versus using signals and slots as part of the Qt5 framework?
Notes:
- The application is multi-threaded, all of the hardware objects exist on the main thread. So communication between threads, in general, is not needed.
- The design that is currently implemented is only used internally for the specific software we are developing. It is not a "public" API that is exposed to our users. From a user's perspective, the control software is a black box.