Как избежать понижения в этом конкретном дизайне иерархии классов?

У меня есть задание создать своего рода мультиплатформенную библиотеку C++ GUI. Он охватывает разные графические интерфейсы на разных платформах. Сама библиотека предоставляет интерфейс, через который пользователь общается единообразно, независимо от используемой им платформы.

Мне нужно правильно спроектировать этот интерфейс и базовую связь с фреймворком. Что я пробовал:

  1. Идиома Pimpl - это решение было выбрано сначала из-за его преимуществ - бинарная совместимость, вырезание дерева зависимостей для увеличения времени сборки...
class Base {
public:
    virtual void show();
    // other common methods
private:
    class impl;
    impl* pimpl_;
};

#ifdef Framework_A
class Base::impl : public FrameWorkABase{ /* underlying platform A code */ };
#elif Framework_B
class Base::impl : public FrameWorkBBase { /* underlying platform B code */ };
#endif

class Button : public Base {
public:
    void click();
private:
    class impl;
    impl* pimpl_;
};

#ifdef Framework_A
class Button::impl : public FrameWorkAButton{ /* underlying platform A code */ };
#elif Framework_B
class Button::impl : public FrameWorkBButton { /* underlying platform B code */ };
#endif

Однако, насколько я понимаю, этот шаблон не был разработан для такой сложной иерархии, где вы можете легко расширить как интерфейсный объект, так и его реализацию. Например. если пользователь хочет создать подкласс кнопки из библиотеки UserButton : Button, ему нужно знать особенности шаблона идиомы pimpl, чтобы правильно инициализировать реализацию.

  1. Простой указатель реализации - пользователю не нужно знать базовый дизайн библиотеки - если он хочет создать собственный элемент управления, он просто создает подклассы элемента управления библиотеки, а обо всем остальном позаботится библиотека.
#ifdef Framework_A
using implptr = FrameWorkABase;
#elif Framework_B
using implptr = FrameWorkBBase;
#endif

class Base {
public:
    void show();
protected:
    implptr* pimpl_;
};

class Button : public Base {
public:
    void click() {
#ifdef Framework_A
        pimpl_->clickA(); // not working, need to downcast
#elif Framework_B
        // works, but it's a sign of a bad design
        (static_cast<FrameWorkBButton>(pimpl_))->clickB();
#endif
    }
};

Поскольку реализация защищена, один и тот же объект implptr будет использоваться в Button — это возможно, потому что и FrameWorkAButton, и FrameWorkBButton наследуются от FrameWorkABBase и FrameWorkABase соответственно. Проблема с этим решением заключается в том, что каждый раз, когда мне нужно позвонить, например. в классе Button что-то вроде pimpl_->click(), мне нужно преобразовать pimpl_, потому что метод clickA() находится не в FrameWorkABase, а в FrameWorkAButton, поэтому он будет выглядеть так (static_cast<FrameWorkAButton>(pimpl_))->click(). А чрезмерное занижение — признак плохого дизайна. Шаблон посетителя в этом случае неприемлем, так как должен быть метод посещения для всех методов, поддерживаемых классом Button и целой кучей других классов.

Может ли кто-нибудь сказать мне, как изменить эти решения или, может быть, предложить какие-то другие, которые будут иметь больше смысла в этом контексте? Заранее спасибо.

EDIT на основе ответа @ruakh

Таким образом, решение pimpl будет выглядеть так:

class baseimpl; // forward declaration (can create this in some factory)
class Base {
public:
    Base(baseimpl* bi) : pimpl_ { bi } {}
    virtual void show();
    // other common methods
private:
    baseimpl* pimpl_;
};

#ifdef Framework_A
class baseimpl : public FrameWorkABase{ /* underlying platform A code */ };
#elif Framework_B
class baseimpl : public FrameWorkBBase { /* underlying platform B code */ };
#endif


class buttonimpl; // forward declaration (can create this in some factory)
class Button : public Base {
public:
    Button(buttonimpl* bi) : Base(bi), // this won't work
                             pimpl_ { bi } {}
    void click();
private:
    buttonimpl* pimpl_;
};

#ifdef Framework_A
class Button::impl : public FrameWorkAButton{ /* underlying platform A code */ };
#elif Framework_B
class Button::impl : public FrameWorkBButton { /* underlying platform B code */ };
#endif

Проблема в том, что вызов Base(bi) внутри ctor Button не сработает, так как buttonimpl не наследует baseimpl, а только его подкласс FrameWorkABase.


person mrdecompilator    schedule 19.03.2020    source источник
comment
У вас не может быть двух разных определений Base::impl (или, во втором подходе, impl) в одной и той же программе. В зависимости от того, как вы это сделаете, результатом будет либо диагностируемая ошибка (если оба определения видны в одной и той же единице компиляции), либо неопределенное поведение (если определения находятся в разных единицах компиляции).   -  person Peter    schedule 19.03.2020
comment
Похоже, вы используете наследование для вещей, для которых оно на самом деле не нужно. Например, зачем вам базовый класс FrameWorkABase?   -  person ruakh    schedule 19.03.2020
comment
@ruakh Для представления базового объекта для управления графическим интерфейсом - например. в Qt это QWidget. Затем вы можете определить такие методы, как show() или getPosition(), общие для всех элементов управления.   -  person mrdecompilator    schedule 19.03.2020
comment
Глядя на этот фрагмент кода в Base: impl* pimpl_; В тех местах, где ваш код знает о FrameWorkAButton, почему бы вам не использовать это вместо этого: FrameWorkAButton* pimpl_; ?   -  person armagedescu    schedule 19.03.2020
comment
@ruakh согласно вашим ответам здесь, FrameWorkABase - это общий объект управления графическим интерфейсом. Если это так, я полагаю, что каждый объект управления является кликабельным, что означает, что класс FrameWorkABase должен иметь виртуальную функцию click(), и каждый подкласс должен реализовывать свой собственный метод щелчка. Таким образом, вы используете полиморфизм для вызова определенного поведения без понижения (что в данном случае является плохим дизайном, поскольку создает зависимости).   -  person SubMachine    schedule 19.03.2020
comment
click() должен был быть методом, специфичным только для кнопок, но я думаю, что это был плохой пример. Как насчет setValue() для счетчика или addItem() для списка? Они не могут быть в классе Base.   -  person mrdecompilator    schedule 19.03.2020
comment
Итак, для ясности - является ли FrameWorkBButton (например) классом, который вы создаете, чтобы он был прокладкой между вашим классом Button и функциональностью кнопки базовой платформы? Или это класс в базовой структуре? Я спрашиваю, потому что в первом случае я не понимаю, почему FrameWorkAButton и FrameWorkBButton предоставляют разные интерфейсы, а во втором случае я не думаю, что вы можете предположить, что каждая нижележащая структура имеет одинаковую иерархию классов (или даже имеет какую-либо иерархия классов — что, если она написана для совместимости с C)?   -  person ruakh    schedule 20.03.2020
comment
@ruakh FrameWorkAButton представляет кнопку пользовательского интерфейса фреймворка, которая отображается в пользовательском интерфейсе. Вы можете представить Framework_A как Qt, FrameWorkABase как QWidget и FrameWorkAButton как QPushButton. В Qt QPushButton наследуется от QWidget, поэтому я могу использовать этот тип полиморфизма. и этот тип иерархии применим к каждой структуре, которую я собираюсь использовать. Мои классы Base или Button просто представляют собой интерфейс, через который я могу взаимодействовать с каждым из элементов управления фреймворка.   -  person mrdecompilator    schedule 21.03.2020
comment
Я думаю, вы ищете встроенные пространства имен.   -  person L. F.    schedule 21.03.2020
comment
@ Л.Ф. Я не совсем уверен, как вы думаете, я должен использовать их. Они могли бы помочь мне с версией моей библиотеки пользовательского интерфейса, но как это решит мою проблему дизайна, связанную с иерархией классов?   -  person mrdecompilator    schedule 21.03.2020


Ответы (1)


Проблема с этим решением заключается в том, что каждый раз, когда мне нужно позвонить, например. в классе Button что-то вроде pimpl_->click() мне нужно преобразовать pimpl_, потому что метод clickA() находится не в FrameWorkABase, а в FrameWorkAButton, поэтому он будет выглядеть так (static_cast<FrameWorkAButton>(pimpl_))->click().

Я могу придумать три способа решить эту проблему:

  1. Исключите Base::pimpl_ в пользу чистой виртуальной защищенной функции Base::pimpl_(). Пусть подклассы реализуют эту функцию, чтобы предоставить указатель реализации на Base::show (и любые другие функции базового класса, которым это необходимо).
  2. Сделайте Base::pimpl_ закрытым, а не защищенным, и дайте подклассам собственную правильно типизированную копию указателя реализации. (Поскольку подклассы отвечают за вызов конструктора базового класса, они могут гарантировать, что они передают ему тот же указатель реализации, который они планируют использовать.)
  3. Сделайте Base::show чистой виртуальной функцией (как и любые другие функции базового класса) и реализуйте ее в подклассах. Если это приводит к дублированию кода, создайте отдельную вспомогательную функцию, которую могут использовать подклассы.

Я думаю, что № 3 — лучший подход, потому что он позволяет избежать привязки вашей иерархии классов к иерархиям классов базовых фреймворков; но я подозреваю из ваших комментариев выше, что вы не согласитесь. Это нормально.


Например. если пользователь хочет создать подкласс кнопки из библиотеки UserButton : Button, ему нужно знать особенности шаблона идиомы pimpl, чтобы правильно инициализировать реализацию.

Независимо от вашего подхода, если вы не хотите, чтобы клиентский код устанавливал указатель реализации (поскольку это означает взаимодействие с базовой структурой), вам нужно будет предоставить конструкторы или фабричные методы, которые делают это. Поскольку вы хотите поддерживать наследование клиентским кодом, это означает предоставление конструкторов, которые обрабатывают это. Так что я думаю, что вы слишком быстро списали идиому Pimpl.


Что касается вашего редактирования — вместо того, чтобы иметь Base::impl и Button::impl extend FrameworkABase и FrameworkAButton, вы должны сделать FrameworkAButton членом данных для Button:: impl и дайте Base::impl просто указатель на него. (Или вы можете дать Button::impl std::unique_ptr для FrameworkAButton вместо того, чтобы удерживать его напрямую; это немного упрощает передачу указателя на Base::impl четко определенным способом.)

Например:

#include <memory>

//////////////////// HEADER ////////////////////

class Base {
public:
    virtual ~Base() { }
protected:
    class impl;
    Base(std::unique_ptr<impl> &&);
private:
    std::unique_ptr<impl> const pImpl;
};

class Button : public Base {
public:
    Button(int);
    virtual ~Button() { }
    class impl;
private:
    std::unique_ptr<impl> pImpl;
    Button(std::unique_ptr<impl> &&);
};

/////////////////// FRAMEWORK //////////////////

class FrameworkABase {
public:
    virtual ~FrameworkABase() { }
};

class FrameworkAButton : public FrameworkABase {
public:
    FrameworkAButton(int) {
        // just a dummy constructor, to show how Button's constructor gets wired
        // up to this one
    }
};

///////////////////// IMPL /////////////////////

class Base::impl {
public:
    // non-owning pointer, because a subclass impl (e.g. Button::impl) holds an
    // owning pointer:
    FrameworkABase * const pFrameworkImpl;

    impl(FrameworkABase * const pFrameworkImpl)
        : pFrameworkImpl(pFrameworkImpl) { }
};

Base::Base(std::unique_ptr<Base::impl> && pImpl)
    : pImpl(std::move(pImpl)) { }

class Button::impl {
public:
    std::unique_ptr<FrameworkAButton> const pFrameworkImpl;

    impl(std::unique_ptr<FrameworkAButton> && pFrameworkImpl)
        : pFrameworkImpl(std::move(pFrameworkImpl)) { }
};

static std::unique_ptr<FrameworkAButton> makeFrameworkAButton(int const arg) {
    return std::make_unique<FrameworkAButton>(arg);
}

Button::Button(std::unique_ptr<Button::impl> && pImpl)
    : Base(std::make_unique<Base::impl>(pImpl->pFrameworkImpl.get())),
      pImpl(std::move(pImpl)) { }
Button::Button(int const arg)
    : Button(std::make_unique<Button::impl>(makeFrameworkAButton(arg))) { }

///////////////////// MAIN /////////////////////

int main() {
    Button myButton(3);
    return 0;
}
person ruakh    schedule 21.03.2020
comment
Спасибо за ответ. Что касается 1-го пункта - ofc, это сведет к минимуму количество понижений, но само понижение все равно нужно будет выполнять в методе pimpl(). 2-й пункт немного напоминает мне решение идиомы pimpl, и мне он нравится больше всего из трех, потому что 3-й пункт не полностью соответствует ООП. Этот комментарий о фабриках и конструкторах заставил меня задуматься, и я думаю, что это правильный путь. НО, мне понадобится предварительное объявление класса impl, чтобы иметь возможность создавать свои объекты вне, например. Button класс, не так ли? (не проблема, просто успокоение) - person mrdecompilator; 22.03.2020
comment
И под опережающим объявлением я подразумеваю объявление вне класса (а не как приватное поле), чтобы этот класс можно было создать на фабрике. Кроме того, все частные переменные должны будут перейти от классов Base и Button к их реализациям (это создаст дублированные данные - например, Base имеет обработчики событий частного поля, которые представляют собой карту определяемых пользователем функций, запускаемых событием, таким как нажатие кнопки - для этого потребуется быть объявленным в каждом классе Base::impl для каждой структуры). - person mrdecompilator; 22.03.2020
comment
Кроме того, я думаю, что не может быть Base::impl, так как Button::impl больше не будет наследоваться от реализации Base, потому что Base::impl : FrameworkABase, Button::impl : FrameworkAButton и FrameworkAButton : FrameworkABase. Поэтому я не смог бы передать Button::impl конструктору Base и понизить его до Button::impl. Я предлагаю, что Base не будет реализовывать идиому pimpl, у него будет базовый указатель, как в моем решении 2. вопросов, но подклассы Base, с другой стороны, будут реализовывать идиому pimpl. - person mrdecompilator; 22.03.2020
comment
Re: Что касается 1-го пункта - ofc, это сведет к минимуму количество приведения к понижению, но само приведение в любом случае должно быть выполнено в методе pimpl(): Вовсе нет; Я не уверен, почему ты так говоришь. Поскольку поле pimpl находится в подклассе, с самого начала оно имеет правильный тип; понижение не требуется. - person ruakh; 22.03.2020
comment
Ах да, конечно, вы правы - из-за ковариантного возвращаемого типа. - person mrdecompilator; 22.03.2020
comment
Я отредактировал свой вопрос - добавил решение pimpl, которое иллюстрирует проблему, которую я описал в комментариях выше. - person mrdecompilator; 24.03.2020
comment
@mrdecompilator: я отредактировал свой ответ, чтобы ответить на ваше редактирование. - person ruakh; 27.03.2020
comment
Но теперь вы не можете переопределить ни один из методов FrameworkAButton, уже реализованных фреймворком. Я мог бы создать что-то вроде AbstractButton::impl, где его член pFrameworkImpl был бы QAbstractButton в Qt. Тогда я смогу расширить AbstractButton на RadioButton или PushButton. Затем pFrameworkImpl RadioButton::impl может расширить QRadioButton. Но это разрешено и в моем решении, где Button::impl представляет собой элемент управления фреймворком, без pFrameworkImpl. Я все равно приму ваш ответ, спасибо за помощь. - person mrdecompilator; 01.04.2020