如何获得解耦的多态行为

C++: How to get decoupled polymorphic behavior

本文关键字:多态 何获得 解耦      更新时间:2023-10-16

在c++中的Qt项目中,我正在使用QtPlugin编写动态加载插件的接口。这个接口应该允许插件注册它们不同的参数,当插件加载时,主程序应该显示代表每个参数的适当GUI控件。例如,参数可以是0到20之间的整数(由QLabel和框中的QSlider表示),或者由QColorDialog表示的颜色值。

这里有个问题:我尝试了一种标准的OOP方法,让每个参数类型继承一个抽象类,并通过实现一个虚函数来创建GUI表示。这导致大量Qt GUI头文件被链接到每个插件文件中,将其大小从~ 20kb增加到~ 50kb。

这不是为了节省几千字节,而是为了更好地理解OOP。我考虑了这个问题,并试图找到合适的设计模式,然后我在谷歌上搜索了"解耦多态性","外部多态性"等,并遇到了一个页面,上面说这是可能的,但通常你不想去那里,因为它破坏了OOP。就这样吗?要么我从插件接口隐藏GUI代码,用枚举或其他东西识别每种类型,并"打破OOP",或者类完全对自己负责,但也完全在内部耦合?

如果每个参数类型都由数据模型、持久性和带有信号的GUI控件组成,您会推荐什么解决方案?什么去哪了?

编辑:换句话说,我想知道插件是否可以是纯数据和算法,不知道数据控件是如何在Qt中创建的,独立于Qt GUI头。它可能使用Q_OBJECT作为信号

我建议让插件去关心它的参数的类型,并且有一个单独的组件知道如何将每种类型映射到GUI控件上。

这几乎是一个直接的模型/视图分解,所以看起来是一个很容易理解的习惯用法。

现在,您的类型模型可以被枚举,或者您可以使用更有争议的OO访问者模式,但是您仍然在实质上提前提出了一个固定的且非真正可扩展的类型系统。这样足够吗?

你可能会以某种类型结束,它既知道给定参数的特定派生类型,也知道如何在Qt中呈现它的细节。这将处理Qt信号,并将值传递回参数。


…通过尝试dynamic_cast或读取某种标识码(如enum),我在想。我仍然不明白如何使用访客DP来代替这些…

访问者模式是,特别是用于避免dynamic_cast,所以我不确定这里的混乱是什么。不可否认,有一个post-hoc版本确实使用了dynamic_cast,但它隐藏在实现中,并且无论如何都不是通常的情况。

因此,对于一个具体的例子,让我们创建一个具有几个参数类型的模型:
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...
};

现在我们有了一些可供它访问的具体类型,我们可以编写抽象的visitor

struct ArgumentHandler {
    virtual ~ArgumentHandler() {}
    virtual void handleInteger(IntegerArgument&);
    virtual void handleBoundedInteger(BoundedIntegerArgument&);
    // ...
};

和具体类型实现如下访问:

void IntegerArgument::visit(ArgumentHandler& handler) {
    hander.handleInteger(*this);
}
void BoundedIntegerArgument::visit(ArgumentHandler& handler) {
    hander.handleBoundedInteger(*this);
}

现在,我们可以只根据数据模型类型编写一个抽象插件——它不需要知道任何关于GUI工具包的知识。假设我们现在只提供一种方法来查询它的参数(注意每个具体子类型都应该有set/get方法)

class PluginBase
{
public:
    virtual int arg_count() const =  0;
    virtual Argument& arg(int n) =  0;
};

最后,我们可以勾勒出一个视图,它知道如何询问抽象插件的参数,如何显示每个具体的参数类型,以及如何处理输入:

// 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);
        }
    }
};

我省略了所有的虚析构函数、Qt信号处理等等。但是,希望您可以看到QtView::IntegerSpinBox对象如何处理valueChanged信号,并调用model_.set()将其推回插件。

您可以将任何类型的消息发送到任何地方,并在另一边使用具有模板的虚拟包捕获它,这些虚拟包正是为与任何对象的松耦合而制作的。

如果我没理解错的话,你应该重新考虑你的行为。而不是让模块在主应用程序中注册所有内容(这可能真的很多),你可以为模块特定的渲染器创建一个基类,并在每个模块中创建一个工厂,以实例化模块的具体渲染器。然后,您可以要求模块呈现您提供给该模块的信息。