【问题标题】:Data abstraction that really allows isolating implementation from the user in C++真正允许在 C++ 中将实现与用户隔离的数据抽象
【发布时间】:2015-10-21 02:52:54
【问题描述】:

我不敢问这个问题,因为它很简单。除非我找不到解决方案。

我最近尝试编写一个简单的程序,该程序在某种程度上忽略了引擎呈现其 UI 的方式。

理论上一切看起来都很棒,但实际上,理论并没有让我走得太远。

假设我的工具需要一个 IWindow 和 IContainer 来承载 ILabel 和 IButton。那是 4 个 UI 元素。去除其中的每一个都是一项微不足道的任务。我可以用 Qt、Gtk、motif 来创建这些元素中的每一个——你可以命名它。

我知道,为了实现(例如,QtWindow 和 QtContainer)工作,抽象(IWindow 和 IContainer)也必须工作:IWindow 需要能够接受IContainer 作为它的孩子:这需要任何一个

  • 我可以将任何 UI 元素添加到容器中,或者
  • 所有 UI 元素都继承自一个父元素

这是完美解决抽象问题的理论。实践(或实施)是另一回事。为了使实现与抽象一起工作-我认为我可以这样做

  • 用暴露实现(或给出提示)的丑陋调用污染抽象——扼杀抽象的概念,或者
  • 将抽象的转换添加到实现可以理解的内容 (dynamic_cast<>())。
  • 将ISomething 实例的全局映射池添加到 UI 特定元素 (map<IElement*, QtElement*>()),这有点像强制转换,除了我自己完成。

所有这些看起来都很难看。我在这里看不到其他选择 - 这是数据抽象概念实际上失败的地方吗?投射是这里唯一的选择吗?

编辑

我花了一些时间试图提出最佳解决方案,这似乎是 C++ 无法简单完成的事情。不是没有强制转换,也不是使用模板。

我最终想出的解决方案(在处理了很多接口以及如何定义这些接口之后)如下所示:

1。需要有一个参数化的基本接口来定义调用

基本接口(我们称它为TContainerBase 用于容器,TElementBase 用于元素)指定预期由容器或元素实现的方法。那部分很简单。

定义需要遵循以下思路:

template <typename Parent>
class TElementBase : public Parent {
    virtual void DoSomething() = 0;
};

template <typename Parent>
class TContainerBase : public Parent {
    virtual void AddElement(TElementBase<Parent>* element) = 0;
};

2。需要有一个指定继承的模板。

这就是分离的第一阶段(引擎与用户界面)来的地方。此时,驱动渲染的后端类型无关紧要。这是有趣的部分:在我看来,唯一成功实现这一点的语言是 Java。模板必须看起来像这样:

一般:

template<typename Engine>
class TContainer : public TContainerBase<Parent> {
    void AddElement(TElementBase<Parent>* element) {
        // ...
    }
};

template<typename Engine>
class TElement : public TElementBase<Parent> {
    void DoSomething() {
        // ...
    }
};

3。 UI 需要能够仅接受 TContainers 或 TElements

也就是说,它必须忽略这些元素的来源。这是分离的第二阶段;毕竟它关心的是TElementBase 和TContainerBase 接口。在 Java 中,通过引入问号已解决。就我而言,我可以简单地在我的 UI 中使用:

TContainer<?> some_container;
TElement<?> some_element;
container.AddElement(&element);

vtable 中的虚函数调用没有问题,因为它们正是编译器所期望的。唯一的问题是确保模板参数在两种情况下都相同。假设后端是一个单一的库 - 这会很好。

上述三个步骤可以让我完全(并且安全地)编写代码而无需考虑后端,而后端几乎可以实现任何需要的东西。

我尝试了这种方法,它变得非常理智。唯一的限制是编译器。实例化类并在这里来回转换它们是违反直觉的,但不幸的是,这是必要的,主要是因为使用模板继承,您不能只提取基类本身,也就是说,您不能说任何一个:

class IContainerBase {};

template <typename Parent>
class TContainerBase : public (IContainerBase : public Parent) {}

也没有

class IContainerBase {};
template <typename Parent>
typedef class IContainerBase : public Parent TContainerBase;

(请注意,在上述所有解决方案中,感觉完全自然而理智地依赖 TElementBase 和 TContainerBase - 如果您将 TElementBase&lt;Foo&gt; 转换为TElementBase&lt;Bar&gt; - 所以这只是语言限制)。

无论如何,这些最终语句(类 A 的 typedef 继承自 B 和类 X 的基类 A 继承自 B)在 C++ 中只是垃圾(并且会使语言变得比现在更难),因此唯一的出路是遵循提供的解决方案之一,我非常感谢。

感谢大家的帮助。

【问题讨论】:

  • 尝试从程序的 UI 代码中抽象出程序的逻辑。这样数据表就不会直接呈现给 UI。相反,它包含某种抽象组件,它可以做到这一点。该组件可以在逻辑不关心的地方针对不同的 UI 引擎进行定制。
  • 如果QtWindow 实现IWindow 和GtkContainer 实现IContainer,是什么阻止我添加GtkContainer 作为QtWindow 的子代?我认为这是您问题的根源:您已经从 彼此 中抽象出不同类型的 UI 组件,而不仅仅是从应用程序中。
  • 对上面的一些澄清:我正在努力解决的问题是IContainer和IElement之间的边界,换句话说,我可以将方法:IContainer::Add(IElement*)添加到抽象中,但是——实施呢?显然,实现也将接收IElement*,然后它必须强制转换,否则IElement 将需要导出一个自定义结构,以便UI 实现可以理解。难道只有这两种方式吗?这就是我失败的地方......
  • 是的,您需要在胶水层中进行铸模。每个 UI 元素类一次转换(或仅一次转换,如果您将其模板化)几乎不会造成灾难。

标签: c++ abstraction


【解决方案1】:

您在这里尝试使用面向对象。 OO 有一种实现通用代码的特殊方法:通过类型擦除。 IWindow 基类接口删除了确切的类型,在您的示例中为QtWindow。在 C++ 中,您可以通过 RTTI 取回一些已擦除的类型信息,即dynamic_cast。

但是,在 C++ 中,您也可以使用模板。不要实现IWindow和QtWindow,而是实现Window&lt;Qt&gt;。这允许您声明Container&lt;Foo&gt; 接受Window&lt;Foo&gt; 用于任何可能的Foo 窗口库。编译器会强制执行。

【讨论】:

    【解决方案2】:

    如果我正确理解您的问题,这就是 Abstract Factory Pattern 旨在解决的那种情况。

    抽象工厂模式提供了一种封装一组具有共同主题的独立工厂的方法,而无需指定它们的具体类。在正常使用中,客户端软件创建抽象工厂的具体实现,然后使用工厂的通用接口来创建作为主题一部分的具体对象。客户不知道(或关心)它从这些内部工厂中的每一个获得哪些具体对象,因为它仅使用其产品的通用接口。这种模式将一组对象的实现细节与它们的一般用法分开,并依赖于对象组合,因为对象创建是在工厂接口中公开的方法中实现的。

    【讨论】:

    • 非常接近。如果我的目标是仅涵盖单个 UI 引擎,那肯定会奏效——这种设计模式仅适用于 Qt。或者只是 Gtk。我可以添加方法,比如IElement::GetGtkWidget(),它允许IContainer 接受任何实现IElement 接口的人。如果你再上一层,也就是说,你想完全与 UI 引擎无关,那么问题就来了:你能确保你的抽象实现知道如何在不强制转换的情况下访问它的对象吗?
    • 抽象工厂使您的客户无需了解或引用您的 IWindow 等具体实现类型,但是是的,您仍然需要 dynamic_cast 和 IContainer 到 QtContainer在某些情况下,在您的 QtWindow 中。这在某种程度上是不可避免的 - 如果您正在做特定于实现的事情,那么您需要处理具体的实现。如果您正在做非特定于实现的事情,那么它们应该被拉到基类中,您可以在其中对抽象类型进行操作。
    【解决方案3】:

    创建一个能够抽象 Qt 和 Gtk 等库的包装器对我来说似乎不是一件容易的事。但是更一般地谈论您的设计问题,也许您可​​以使用模板来进行抽象接口和特定实现之间的映射。例如:

    抽象界面IWidget.h

    template<typename BackendT>
    class IWidget
    {
    public:
        void doSomething()
        {
            backend.doSomething();
        }
    
    private:
        BackendT backend;
    };
    

    Qt实现QtWidget.h:

    class QtWidget
    {
    public:
        void doSomething()
        {
            // qt specifics here
            cout << "qt widget" << endl;
        }
    };
    

    Gtk 实现GtkWidget.h:

    class GtkWidget
    {
    public:
        void doSomething()
        {
            // gtk specifics here
            cout << "gtk widget" << endl;
        }
    };
    

    Qt 后端QtBackend.h:

    #include "QtWidget.h"
    // include all the other gtk classes you implemented...
    #include "IWidget.h"
    
    typedef IWidget<QtWidget> Widget;
    // map all the other classes...
    

    Gtk 后端GtkBackend.h:

    #include "GtkWidget.h"
    // include all the other gtk classes you implemented...
    #include "IWidget.h"
    
    typedef IWidget<GtkWidget> Widget;
    // map all the other classes...
    

    应用:

    // Choose the backend here:
    #include "QtBackend.h"
    
    int main()
    {
        Widget* w = new Widget();
        w->doSomething();
    
        return 0;
    }
    

    【讨论】:

    • 我已经做到了。问题出现在下一个阶段:假设您有容器和元素要添加到该容器中。您将如何设计将元素添加到容器的方法,这样您就不必在内部进行强制转换?
    • 这只允许在编译时选择一个后端。
    • @TomaszWiszkowski 在这个实现中,没有IButton 类。有IButton&lt;GtkBitton&gt;、IButton&lt;QtButton&gt; 等。IButton 是其中一个的 typedef,在编译时选择。
    猜你喜欢
    • 1970-01-01
    • 2010-10-10
    • 2019-06-09
    • 1970-01-01
    • 1970-01-01
    • 2016-05-06
    • 1970-01-01
    • 1970-01-01
    • 2017-06-18
    相关资源
    最近更新 更多