扩展一个共同的基类:钻石继承 vs. QObject

10

我觉得我在这里遇到了一种钻石继承问题。

Qt提供了几个旋转框,用于整数值、双精度和日期/时间。它们都派生自QAbstractSpinBox

#include <QtWidgets/QSpinBox>
class QSpinBox:
    public QAbstractSpinBox {

};

#include <QtWidgets/QDoubleSpinBox>
class QDoubleSpinBox:
    public QAbstractSpinBox {

};

现在我想为所有旋转框添加一些共同的功能,在这个具体的例子中,是一个按钮把旋转框恢复到其最小值(因此是特殊值文本)。 因此,我也从QAbstractSpinBox派生出来,并想出了类似于这样的东西:

class AbstractRevertibleSpinBox:
    public QAbstractSpinBox {

    public:
        RevertibleSpinBox() {
            /* Use abstract base: */
            QAction *revertAction = new QAction(this);
            QAbstractSpinBox::lineEdit()->addAction(
                revertAction, QLineEdit::TrailingAction);
            /* ... */
        }

    public slots:
        virtual void revert()  = 0;
}

这包含了纯粹的 revert(),旨在实现如何还原不同的数字框。例如,对于 QDoubleSpinBox,使用 setValue(double),对于 QDateEdit,使用 setDate(QDate)

然后,我选择了显而易见的方式,并为所有我需要的数字框派生了适当的类,就像这些:

class RevertibleSpinBox:
    public QSpinBox,
    public AbstractRevertibleSpinBox {

    protected:
        void revert() {
            /* Revert 'int' */
            setValue(0);
        }
};

class RevertibleDoubleSpinBox:
    public QDoubleSpinBox,
    public AbstractRevertibleSpinBox {

    protected:
        void revert() {
            /* Revert 'double' */
            setValue(0.0);
        }
};

显然这种方法不起作用,因为QAbstractSpinBox中的任何内容现在都是模糊的。我以为可以使用虚继承来解决这个问题,例如QDoubleSpinBox会从自己的QAbstractSpinBox进行虚继承,但它并没有这样做。此外,对于QObject,也会失败,因为Qt似乎在那里进行了许多向上的static_cast,而这也不能与虚继承一起使用。 我还想通过将AbstractRevertibleSpinBox作为模板类来解决它,该模板类将作为模板类参数传递不同的旋转框类型。构造将如下所示:

template<class Base>
class AbstractRevertibleSpinBox:
    public Base {};

class RevertibleSpinBox:
    public AbstractRevertibleSpinBox<SpinBox> { };

虽然这种方法可以工作,但是Qt的moc对模板类非常不满意。例如,在模板类内部无法使用传统的基于字符串的SIGNAL()/SLOT()语法连接任何信号和槽。

有没有其他比较优雅的方法来解决这个问题呢?


1
好问题,如果我没有达到每日投票限制的话,我会点赞的... 为什么不使用装饰器设计模式呢?这看起来是一个很好的使用案例。 - László Papp
问题在于我需要从QAbstractSpinBoxBase中获取受保护的方法,比如lineEdit()。我可以通过创建一个自定义基类来解决这个问题,该基类派生自QAbstractSpinBoxBase并提供对受保护方法的公共API。此外,我希望能够避免将所有较低级别的旋转框方法复制到包装类中的样板文件中 :-/ 谢谢,这是我在这里的第一个问题 :-) - Kamajii
期待更多地了解这个模式。谢谢! - Kamajii
请不要在没有给予他人解决问题的功劳的情况下编辑您的问题。这不是 Stack Overflow 上提问的正确方式。 - László Papp
我本来想编辑问题,以给予迄今为止所有做出贡献的人以荣誉。不幸的是,目前问题尚未解决。(顺便说一下,在下面你告诉我不要将半问句作为答案发布。好的。Mohit Jain以及一些meta上的帖子建议通过更新来修改问题。好的。你告诉我不要这样做。那么在哪里修改源摘录才是好的呢?我敢打赌使用某些粘贴板也会引起批评。) - Kamajii
显示剩余2条评论
4个回答

7
正如我在评论中提到的那样,如果您想要一个易于扩展的功能系统,那么使用装饰者模式是一个明显的选择。否则,只需从QObject继承而不是从基本“接口”继承即可,代码几乎相同。
我将从其他答案中提供的IMHO较差的方法开始:
- 子类化每个spin box
这显然非常繁琐,更重要的是,您将无法支持任何QSpinBox子类,因为您总是需要为每个添加创建新的子类。这只是一种不灵活的方法。
- 有一个包含按钮和旋转框的父小部件
这看起来像是两个不同事物的不必要耦合,因此,如果您稍后通过按钮以外的方式触发它们,您将无法轻松地重用旋转框。我认为这两个概念应该保持不同并分别管理。
此外,建议使用qobject_cast而不是dynamic_cast。
让我们更仔细地看看装饰器方法:

enter image description here

这还不是你情况的解决方案,但它很好地展示了如何将功能(即“装饰”)添加到现有层次结构中。为了更具体地讲述你的用例,让我们看看在你的特定场景中会发生什么:
- 组件:QAbstractSpinBox - 具体组件: - QSpinBox - QDoubleSpinBox - QDateTimeEdit - QDateEdit - QTimeEdit - 装饰器:AbstractSpinBoxDecorator(在你的情况下可以省略此步骤) - 具体装饰器:RevertibleSpinBoxDecorator 让我们开始动手实现这个设计吧:

main.cpp

#include <QAbstractSpinBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QDateTimeEdit>
#include <QDateEdit>
#include <QTimeEdit>
#include <QPushButton>
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QWidget>
#include <QShowEvent>

class RevertibleSpinBoxDecorator : public QAbstractSpinBox
{
    Q_OBJECT
public:
    explicit RevertibleSpinBoxDecorator(QAbstractSpinBox *abstractSpinBox, QAbstractSpinBox *parent = Q_NULLPTR)
        : QAbstractSpinBox(parent)
        , m_abstractSpinBox(abstractSpinBox)
    {
    }

public slots:
    void revert(bool)
    {
        QSpinBox *spinBox = qobject_cast<QSpinBox*>(m_abstractSpinBox);
        if (spinBox) {
            spinBox->setValue(spinBox->minimum());
            return;
        }

        QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(m_abstractSpinBox);
        if (doubleSpinBox) {
            doubleSpinBox->setValue(doubleSpinBox->minimum());
            return;
        }

        QDateEdit *dateEdit = qobject_cast<QDateEdit*>(m_abstractSpinBox);
        if (dateEdit) {
            dateEdit->setDate(dateEdit->minimumDate());
            return;
        }

        QTimeEdit *timeEdit = qobject_cast<QTimeEdit*>(m_abstractSpinBox);
        if (timeEdit) {
            timeEdit->setTime(timeEdit->minimumTime());
            return;
        }

        QDateTimeEdit *dateTimeEdit = qobject_cast<QDateTimeEdit*>(m_abstractSpinBox);
        if (dateTimeEdit) {
            dateTimeEdit->setDateTime(dateTimeEdit->minimumDateTime());
            return;
        }

        Q_ASSERT_X(false, "decorator", "concrete component unimplemented");
    }

protected:
    void showEvent(QShowEvent *event) Q_DECL_OVERRIDE
    {
        m_abstractSpinBox->show();
        event->ignore();
        hide();
    }

private:
     QAbstractSpinBox *m_abstractSpinBox;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
        {
            connect(pushButton, &QPushButton::clicked, revertibleSpinBoxDecorator, &RevertibleSpinBoxDecorator::revert);
            QHBoxLayout *layout = new QHBoxLayout(centralWidget);
            layout->addWidget(revertibleSpinBoxDecorator);
            layout->addWidget(pushButton);
            setCentralWidget(centralWidget);
        }

    private:
        QWidget *centralWidget{new QWidget(this)};
        QDoubleSpinBox *doubleSpinBox{new QDoubleSpinBox(this)};
        RevertibleSpinBoxDecorator *revertibleSpinBoxDecorator{new RevertibleSpinBoxDecorator(doubleSpinBox)};
        QPushButton *pushButton{new QPushButton(this)};
};

#include "main.moc"

int main(int argc, char **argv)
{
    QApplication application(argc, argv);
    MainWindow mainWindow;
    mainWindow.show();
    return application.exec();
}

如果要摆脱 QAbstractSpinBox 的继承关系,你需要应用更多的粘合剂,我认为这并没有太大的收获,而是失去了灵活性。你可以从类似于以下内容开始:

非装饰者模式

#include <QAbstractSpinBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QDateTimeEdit>
#include <QDateEdit>
#include <QTimeEdit>
#include <QPushButton>
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QWidget>
#include <QShowEvent>

class RevertibleSpinBoxDecorator : public QObject
{
    Q_OBJECT
public:
    explicit RevertibleSpinBoxDecorator(QAbstractSpinBox *abstractSpinBox, QObject *parent = Q_NULLPTR)
        : QObject(parent)
        , m_abstractSpinBox(abstractSpinBox)
    {
    }

public slots:
    void revert(bool)
    {
        QSpinBox *spinBox = qobject_cast<QSpinBox*>(m_abstractSpinBox);
        if (spinBox) {
            spinBox->setValue(spinBox->minimum());
            return;
        }

        QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(m_abstractSpinBox);
        if (doubleSpinBox) {
            doubleSpinBox->setValue(doubleSpinBox->minimum());
            return;
        }

        QDateEdit *dateEdit = qobject_cast<QDateEdit*>(m_abstractSpinBox);
        if (dateEdit) {
            dateEdit->setDate(dateEdit->minimumDate());
            return;
        }

        QTimeEdit *timeEdit = qobject_cast<QTimeEdit*>(m_abstractSpinBox);
        if (timeEdit) {
            timeEdit->setTime(timeEdit->minimumTime());
            return;
        }

        QDateTimeEdit *dateTimeEdit = qobject_cast<QDateTimeEdit*>(m_abstractSpinBox);
        if (dateTimeEdit) {
            dateTimeEdit->setDateTime(dateTimeEdit->minimumDateTime());
            return;
        }

        Q_ASSERT_X(false, "strategy", "strategy not implemented");
    }

private:
     QAbstractSpinBox *m_abstractSpinBox;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
        {
            connect(pushButton, &QPushButton::clicked, revertibleSpinBoxDecorator, &RevertibleSpinBoxDecorator::revert);
            QHBoxLayout *layout = new QHBoxLayout(centralWidget);
            layout->addWidget(doubleSpinBox);
            layout->addWidget(pushButton);
            setCentralWidget(centralWidget);
        }

    private:
        QWidget *centralWidget{new QWidget(this)};
        QDoubleSpinBox *doubleSpinBox{new QDoubleSpinBox(this)};
        RevertibleSpinBoxDecorator *revertibleSpinBoxDecorator{new RevertibleSpinBoxDecorator(doubleSpinBox)};
        QPushButton *pushButton{new QPushButton(this)};
};

#include "main.moc"

int main(int argc, char **argv)
{
    QApplication application(argc, argv);
    MainWindow mainWindow;
    mainWindow.show();
    return application.exec();
}

main.pro

TEMPLATE = app
TARGET = main
QT += widgets
CONIG += c++11
SOURCES += main.cpp

构建和运行

qmake && make && ./main

1
考虑的一个选项是创建从每个 QSpinBox, QDoubleSpinBoxQDateEdit 派生的类,然后将共同的代码提取到函数中。
QSpinBox 为例:
class RevertibleSpinBox : public QSpinBox
{
public:
    RevertibleSpinBox(QWidget* parent) : QSpinBox(parent)
    {
        RevertibleSpinBoxHelpers::installRevertAction(lineEdit(), this);
    }

public slots:
    void revert()
    {
        setValue(0);
    }

// etc.
};

namespace RevertibleSpinBoxHelpers
{
    void installRevertAction(QLineEdit* target, QObject* handler)
    {
        QAction* revertAction = new QAction(handler);
        target->addAction(revertAction, QLineEdit::TrailingAction);
        QObject::connect(revertAction, SIGNAL(triggered()), handler, SLOT(revert()));
    }
}

免责声明:如果您的需求比您在问题中提到的要复杂,那么这可能不是最佳方法。


谢谢。这是我用来克服模板类信号/槽限制的一种方法。模板化的基类有一个虚拟的connectAction(),最终的类可以实现它来连接它的revert slot。 - Kamajii
@Kamajii:有不使用这个解决方案的原因吗? - Silicomancer
@Silicomancer:创建了太多的类,现在至少有5个。我认为OP不想要那样,我也不想要。 - László Papp
是的,那很糟糕。但我仍然更喜欢那个解决方案。装饰器模式似乎是唯一的选择。但它被定义为在接口上使用。QAbstractSpinBox是抽象的,但它不是一个接口。这就是为什么会出现 ghost-widget 讨论的原因。就个人而言,我更喜欢这 5 个(干净的)类。如果类的数量是问题,那实际上只是懒惰的问题;-) - Silicomancer

1
我认为在这里根本不需要使用继承,而是应该使用组合:创建一个无关的类(例如从QWidget派生),该类使用QBoxLayout(或类似物)将旋转框和按钮排列为子窗口小部件。适当的旋转框对象指针可以传递给类的构造函数,该函数还将执行必要的connect()命令以前后转发各种信号。当然,您可能需要重新声明QAbstractSpinBox类的插槽/信号/方法的等效项,但好处是它将与任何QAbstractSpinBox子类一起使用。
(至于让revert()做正确的事情,最简单的方法可能只是使用dynamic_cast<>的一些丑陋的特殊情况逻辑 - 因为只有三个QAbstractSpinBox子类需要支持,所以这将是可管理的,至少这样丑陋性质被隐藏在私有方法体内,而不是暴露给类的用户)

谢谢Jeremy。然而,你的方法的缺点是我必须“模板化”所有需要从外部访问且在QAbstractSpinBox基类中不可用的较差的旋转框方法,因为它们取决于实际旋转框的类型(int / double / QDate / ...)。希望我在这里是错误的 :-) - Kamajii
如果你想避免太多的重复,我想你可以使用QVariant类作为你的万能数据类型。 - Jeremy Friesner

0

我认为你在不必要地让生活变得复杂了。即使装饰器模式也有点过头了,更不用说那些不必要的继承和设计问题了。你只需要一个巧妙的辅助函数:

void revert(QAbstractSpinBox * box) {
    QSpinBox * spin = qobject_cast<QSpinBox *>(box);
    if (spin) { spin->setValue(spin->minimum()); return; }
    QDateTimeEdit * dt = qobject_cast<QDateTimeEdit *>(box);
    if (dt) { dt->setDateTime(dt->minimumDateTime()); return; }
    // and so on...
    qDebug() << "you should not be seeing this";
    return;
}

如果您想要在旋转框旁边实例化一些内容,可以将其作为Reverter对象的静态方法进行包装。

如果将其放入派生自QObject的类中,可以将其作为槽,但这并不是必需的,因为在Qt中,您可以连接到非QObject派生类的自由函数和方法。或者,如果选择,可以从槽中简单地调用它:

public slots:
    void revertSpinBoxA() { revert(spinBoxA); }
    void revertSpinBoxB() { revert(spinBoxB); }

这类似于装饰器模式,但它具有以下几个优点:

  • 仅为继承层次结构中的所有对象提供额外功能,这意味着您可以节省重新实现现有功能的时间和精力,直接使用传递的对象的功能
  • 您只需要一个Reverter来还原任意数量的 spinboxes,而使用装饰器,则需要 n 个装饰器来处理 n 个 spinboxes,这将占用更多内存,这可能不会导致致命的问题,但是是不必要的开销,因为 revert 方法内部的类型转换使得实际的装饰器完全多余。差异在于装饰器中,您将拥有原始对象+每个对象的装饰器,其中每个对象都用作对象的控制器,而在此解决方案中,您仍然使用实际对象,并仅通过Reverter传递它以获取额外的功能。工作量更少,代码更少,复杂度更低,使用的内存更少。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接