In a Qt project in C++, I am writing an interface for dynamically loaded plugins using QtPlugin. This interface should allow plugins to register their different parameters, and while a plugin is loaded the main program should display appropriate GUI controls representing each parameter. For instance, a parameter could be an int between 0 and 20 a represented by a QLabel and a QSlider in a box, or a color value represented by a QColorDialog.
Here's the catch: I tried a standard OOP approach (?), having each parameter type inherit an abstract class and creating the GUI representation by implementing a virtual function. This caused a lot of Qt GUI headers to get linked into each plugin file, increasing its size from ~20 KB to ~50 KB.
This isn't about saving those kilobytes but about gaining a better understanding of OOP. I thought about this and tried to find suitable design patterns, then I googled "decoupled polymorphism", "external polymorphism" et c and came across a page that said that this is possible but generally you don't want to go there because it breaks OOP.
So is that it? Either I hide GUI code from the plugin interface and identify each type with an enum or something, and "break OOP", or the class is entirely reponsible for itself but also completely coupled internally?
What solutions would you recommend, if each parameter type consists of a data model, persistence, and GUI controls with signals? What goes where?
EDIT: In other words I'm wondering if the plugins can be pure data and algorithms, unaware of how data controls are created in Qt and independent of Qt GUI headers. It might use Q_OBJECT for signals though.
I'd suggest letting the plugin worry about the types of its arguments, and have a separate component which knows how to map each type onto a GUI control.
This is almost a straight model/view decomposition, so seems like a well-understood idiom.
Now, your type model can be enumerated, or you can use the arguably more OO Visitor pattern, but you're still essentially coming up with a fixed and not-really-extensible type system ahead of time. Is that adequate?
You'll probably end up with some type that knows both the specific derived type of a given argument, and the details of how to render it in Qt. This would handle the Qt signals, and pass values back to the argument.
... Through attempting a dynamic_cast or reading some kind of identification code such as an enum, I'm thinking. I still don't see how the Visitor DP could be used instead of these ...
The Visitor pattern is specifically used to avoid dynamic_cast
, so I'm not sure what the confusion is here. Admittedly there's a post-hoc version which does use dynamic_cast
, but that's hidden away in the implementation and isn't the usual case anyway.
So, for a concrete example, let's create a model with a couple of argument types:
struct ArgumentHandler; // visitor
class Argument { // base class for visitable concrete types
public:
virtual void visit(ArgumentHandler&) = 0;
};
// sample concrete types
class IntegerArgument: public Argument {
int value_;
public:
IntegerArgument(int value = 0) : value_(value) {}
void set(int v) { value_ = v; }
int get() const { return value_; }
virtual void visit(ArgumentHandler&);
};
class BoundedIntegerArgument: public IntegerArgument
{
int min_, max_;
public:
virtual void visit(ArgumentHandler&);
// etc...
};
Now we have some concrete types for it to visit, we can write the abstract visitor
struct ArgumentHandler {
virtual ~ArgumentHandler() {}
virtual void handleInteger(IntegerArgument&);
virtual void handleBoundedInteger(BoundedIntegerArgument&);
// ...
};
and our concrete types implement visitation like so:
void IntegerArgument::visit(ArgumentHandler& handler) {
hander.handleInteger(*this);
}
void BoundedIntegerArgument::visit(ArgumentHandler& handler) {
hander.handleBoundedInteger(*this);
}
Now, we can write an abstract plugin only in terms of the data model types - it doesn't need to know anything about the GUI toolkit. Let's say we just provide a way to query its arguments for now (note that each concrete subtype should have set/get methods)
class PluginBase
{
public:
virtual int arg_count() const = 0;
virtual Argument& arg(int n) = 0;
};
Finally, we can sketch a View that knows how to interrogate an abstract plugin for its arguments, how to display each concrete argument type, and how handle inputs:
// concrete renderer
class QtView: public ArgumentHandler
{
struct Control {};
struct IntegerSpinBox: public Control {
QSpinBox control_;
IntegerArgument &model_;
};
struct IntegerSlider: public Control {
QSlider control_;
BoundedIntegerArgument &model_;
};
std::vector<std::unique_ptr<Control>> controls_;
public:
// these overloads know how to render each argument type
virtual void handleInteger(IntegerArgument &arg) {
controls_.push_back(new IntegerSpinBox(arg));
}
virtual void handleBoundedInteger(BoundedIntegerArgument &arg) {
controls_.push_back(new IntegerSlider(arg));
}
// and this is how we invoke them:
explicit QtView(PluginBase &plugin) {
for (int i=0; i < plugin.arg_count(); ++i) {
plugin.arg(i).visit(*this);
}
}
};
I've omitted all the virtual destructors, the Qt signal handling, and lots more. But, hopefully you can see how a QtView::IntegerSpinBox
object could handle the valueChanged
signal from its captive spinbox widget, and call model_.set()
to push that back to the plugin.
You can send message of any type, to anywhere and catch it on the other side with anything with templatious virtual packs which were made exactly for loose-coupling-with-anything purpose.
If I understood you correctly, you should rethink the behaviour. Instead of having the module registering everything (which can be really a lot) in the main app, you could create a base class for a module specific renderer, and a factory in each module, that instantiates the concrete renderer for the module. You then can ask the module to render the information you are providing to the module.