在这种特定的类层次结构设计中,如何避免垂头丧气? [英] How to avoid downcasting in this specific class hierarchy design?

查看:102
本文介绍了在这种特定的类层次结构设计中,如何避免垂头丧气?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经完成了创建一种多平台C ++ GUI库的任务.它在不同平台上包装了不同的GUI框架.该库本身提供了一个界面,无论用户使用什么平台,都可以通过该界面进行统一通信.

I've got an assignment to create a sort of a multi-platform C++ GUI library. It wraps different GUI frameworks on different platforms. The library itself provides an interface via which the user communicates uniformly regardless of the platform he's using.

我需要正确设计此接口以及与框架的基础通信.我尝试过的是:

I need to design this interface and underlying communication with the framework properly. What I've tried is:

  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惯用模式的细节才能正确地初始化实现.

However, to my understanding, this pattern wasn't designed for such a complicated hierarchy where you can easily extend both interface object and its implementation. E.g. if the user wanted to subclass button from the library UserButton : Button, he would need to know the specifics of the pimpl idiom pattern to properly initialize the implementation.

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

由于实现受保护,因此Button中将使用相同的implptr对象-之所以可能,是因为FrameWorkAButtonFrameWorkBButton分别继承于FrameWorkABBaseFrameWorkABase.该解决方案的问题是每次我需要致电例如在Button类中,例如pimpl_->click(),我需要向下转换pimpl_,因为clickA()方法不在FrameWorkABase中,而在FrameWorkAButton中,所以它看起来像(static_cast<FrameWorkAButton>(pimpl_))->click().过度的垂头丧气是不良设计的标志.在这种情况下,访问者模式是不可接受的,因为Button类和所有其他类支持的所有方法都需要一个visit方法.

Since the implementation is protected, the same implptr object will be used in Button - this is possible because both FrameWorkAButton and FrameWorkBButton inherit from FrameWorkABBase and FrameWorkABase respectively. The problem with this solution is that every time i need to call e.g. in Button class something like pimpl_->click(), I need to downcast the pimpl_, because clickA() method is not in FrameWorkABase but in FrameWorkAButton, so it would look like this (static_cast<FrameWorkAButton>(pimpl_))->click(). And excessive downcasting is a sign of bad design. Visitor pattern is unacceptable in this case since there would need to be a visit method for all the methods supported by the Button class and a whole bunch of other classes.

有人可以告诉我,如何修改这些解决方案,或者提出其他建议,在这种情况下会更有意义?预先感谢.

Can somebody please tell me, how to modify these solutions or maybe suggest some other, that would make more sense in this context? Thanks in advance.

编辑基于od @ruakh的答案

EDIT based od @ruakh 's answer

因此pimpl解决方案如下所示:

So the pimpl solution would look like this:

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

与此有关的问题是,在Button的ctor中调用Base(bi)将不起作用,因为buttonimpl不会继承baseimpl,仅是其子类FrameWorkABase.

The problem with this is that calling Base(bi) inside the Button's ctor will not work, since buttonimpl does not inherit baseimpl, only it's subclass FrameWorkABase.

推荐答案

此解决方案的问题是,每次我需要致电例如在Button类中,例如pimpl_->click(),我需要向下转换pimpl_,因为clickA()方法不在FrameWorkABase中,而在FrameWorkAButton中,所以它看起来像(static_cast<FrameWorkAButton>(pimpl_))->click().

The problem with this solution is that every time i need to call e.g. in Button class something like pimpl_->click(), I need to downcast the pimpl_, because clickA() method is not in FrameWorkABase but in FrameWorkAButton, so it would look like this (static_cast<FrameWorkAButton>(pimpl_))->click().

我可以想到三种解决该问题的方法:

I can think of three ways to solve that issue:

  1. 消除Base :: pimpl_,转而使用纯虚拟受保护函数Base :: pimpl_().让子类实现该功能,以提供指向Base :: show的实现指针(以及需要它的任何其他基类函数).
  2. 将Base :: pimpl_设为私有而不是受保护,并为子类提供自己的适当类型的实现指针副本. (由于子类负责调用基类构造函数,因此它们可以确保为其提供与计划使用的实现指针相同的实现指针.)
  3. 使Base :: show是一个纯虚函数(以及其他任何基类函数),并在子类中实现它.如果这样会导致代码重复,请创建一个单独的帮助函数,子类可以使用该帮助函数.

我认为#3是最好的方法,因为它避免了将您的类层次结构与基础框架的类层次结构耦合在一起;但我怀疑您从上述评论中会不同意.很好.

I think that #3 is the best approach, because it avoids coupling your class hierarchy to the class hierarchies of the underlying frameworks; but I suspect from your comments above that you'll disagree. That's fine.

例如如果用户想从库UserButton : Button中继承按钮,他将需要知道pimpl惯用模式的细节才能正确地初始化实现.

E.g. if the user wanted to subclass button from the library UserButton : Button, he would need to know the specifics of the pimpl idiom pattern to properly initialize the implementation.

无论采用哪种方法,如果您都不希望客户端代码必须设置实现指针(因为这意味着要与基础框架进行交互),则需要提供构造函数或工厂方法来实现.由于您希望通过客户端代码支持继承,因此这意味着需要提供处理此问题的构造函数.因此,我认为您过快地取消了Pimpl习惯用法.

Regardless of your approach, if you don't want the client code to have to set up the implementation pointer (since that means interacting with the underlying framework), then you will need to provide constructors or factory methods that do so. Since you want to support inheritance by client code, that means providing constructors that handle this. So I think you wrote off the Pimpl idiom too quickly.

关于您的编辑-而不是让Base :: impl和Button :: impl extend FrameworkABase和FrameworkAButton,您应该使FrameworkAButton成为以下对象的数据成员 Button :: impl,然后给Base :: impl一个指针. (或者您可以给Button :: impl一个std :: unique_ptr到FrameworkAButton而不是直接持有它;这使得以一种明确定义的方式将指针传递给Base :: impl变得容易一些.)

In regards to your edit — rather than having Base::impl and Button::impl extend FrameworkABase and FrameworkAButton, you should make the FrameworkAButton be a data member of Button::impl, and give Base::impl just a pointer to it. (Or you can give Button::impl a std::unique_ptr to the FrameworkAButton instead of holding it directly; that makes it a bit easier to pass the pointer to Base::impl in a well-defined way.)

例如:

#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;
}

这篇关于在这种特定的类层次结构设计中,如何避免垂头丧气?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆