Как избежать уныния в этом специфическом c дизайне иерархии классов? - PullRequest
2 голосов
/ 19 марта 2020

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

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

  1. Pimpl idiom - сначала это решение было выбрано из-за его преимуществ - двоичной совместимости, сокращения дерева зависимостей для увеличения времени сборки ...
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, чтобы правильно инициализировать реализацию.

Простой указатель реализации - пользователю не нужно знать базовый дизайн библиотеки - если он хочет создать пользовательский элемент управления, он просто подклассирует библиотечный элемент управления, а об остальном заботится библиотека
#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
    }
};

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

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

РЕДАКТИРОВАТЬ на основе ответа od @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.

1 Ответ

1 голос
/ 21 марта 2020

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

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

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

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


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

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


Что касается вашего редактирования - вместо того, чтобы использовать Base :: impl и Button :: impl extension 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;
}
...