c++ - 类型应该在面向数据的设计中包含方法吗?

标签 c++ architecture encapsulation data-oriented-design

当前,我的应用程序包含三种类型的类。它应该遵循面向数据的设计,如果不是,请更正我。这是三种类型的类。代码示例不是那么重要,可以根据需要跳过它们。他们只是在那里给人留下深刻的印象。我的问题是,我应该在类型类中添加方法吗?

当前设计

类型仅包含值。

struct Person {
    Person() : Walking(false), Jumping(false) {}
    float Height, Mass;
    bool Walking, Jumping;
};

模块每个模块都实现一种独特的功能。他们可以访问所有类型,因为这些类型是全局存储的。
class Renderer : public Module {
public:
    void Init() {
        // init opengl and glew
        // ...
    }
    void Update() {
        // fetch all instances of one type
        unordered_map<uint64_t, *Model> models = Entity->Get<Model>();
        for (auto i : models) {
            uint64_t id = i.first;
            Model *model = i.second;
            // fetch single instance by id
            Transform *transform = Entity->Get<Transform>(id);
            // transform model and draw
            // ...
        }
    }
private:
    float time;
};

管理器是通过基础Module类注入(inject)到模块中的帮助程序。上面使用的Entity是实体管理器的实例。其他管理器包括消息传递,文件访问,sql存储等。简而言之,应在模块之间共享所有功能。
class ManagerEntity {
public:
    uint64_t New() {
        // generate and return new id
        // ...
    }
    template <typename T>
    void Add(uint64_t Id) {
        // attach new property to given id
        // ...
    }
    template <typename T>
    T* Get(uint64_t Id) {
        // return property attached to id
        // ...
    }
    template <typename T>
    std::unordered_map<uint64_t, T*> Get() {
        // return unordered map of all instances of that type
        // ...
    }
};

问题

现在,您对我当前的设计有了一个想法。现在考虑类型需要更复杂的初始化的情况。例如,Model类型仅为其纹理和顶点缓冲区存储了OpenGL id。实际数据必须先上传到视频卡。
struct Model {
    // vertex buffers
    GLuint Positions, Normals, Texcoords, Elements;
    // textures
    GLuint Diffuse, Normal, Specular;
    // further material properties
    GLfloat Shininess;
};

当前,有一个带有Models函数的Create()模块,它负责建立模型。但是通过这种方式,我只能从该模块创建模型,而不能从其他模块创建模型。我是否应该在将其复杂化的同时将其移至类型类Model?我虽然把类型定义当作接口(interface)一样。

最佳答案

首先,您不必在所有地方都应用面向数据的设计。最终,它是一种优化,甚至对性能至关重要的代码库仍然有很多部分无法从中受益。

我倾向于经常将其视为消除结构,而使用大数据块来进行更有效的处理。以图像为例。为了有效地表示其像素,通常需要存储一个简单的数值数组,而不是存储用户定义的抽象像素对象的集合,这些对象具有虚拟指针作为夸张的示例。

想象一下使用浮点数的4分量(RGBA)32位图像,但由于任何原因仅使用8位alpha(对不起,这是一个愚蠢的示例)。如果我们甚至为像素类型使用基本的struct,由于对齐需要使用结构填充,通常最终需要使用像素结构的内存更多。

struct Image
{
    struct Pixel
    {
        float r;
        float g;
        float b;
        unsigned char alpha;
        // some padding (3 bytes, e.g., assuming 32-bit alignment
        // for floats and 8-bit alignment for unsigned char)
    };
    vector<Pixel> Pixels;
};

即使在这种简单情况下,将其转换为带有8位alpha并行数组的浮点平面数组也可以减小内存大小,并有可能因此提高顺序访问速度。
struct Image
{
    vector<float> rgb;
    vector<unsigned char> alpha;
};

...这就是我们最初应该考虑的方式:关于数据,内存布局。当然,通常已经有效地表示了图像,并且已经实现了图像处理算法来批量处理大量像素。

然而,面向数据的设计通过将这种表示形式甚至应用到比像素高得多的事物上,将其带到了比平常更高的层次。以类似的方式,您可能会受益于对ParticleSystem而不是单个Particle建模,从而为优化留有喘息的空间,甚至为People而不是Person留有喘息的空间。

但是,让我们回到图像示例。这往往意味着缺少DOD:
struct Image
{
    struct Pixel
    {
        // Adjust the brightness of this pixel.
        void adjust_brightness(float amount);

        float r;
        float g;
        float b;
    };
    vector<Pixel> Pixels;
};

这种adjust_brightness方法的问题是,从接口(interface) Angular 来看,它被设计为可在单个像素上工作。这可能使得难以应用从一次访问多个像素中受益的优化和算法。同时,如下所示:
struct Image
{
    vector<float> rgb;
};
void adjust_brightness(Image& img, float amount);

...可以通过受益于一次访问多个像素的方式来编写。我们甚至可以用SoA代表这样表示:
struct Image
{
    vector<float> r;
    vector<float> g;
    vector<float> b;
};

...如果您的热点与顺序处理有关,这可能是最佳选择。细节无关紧要。对我来说,重要的是您的设计留有喘息的空间来进行优化。 DOD给我的值(value)实际上是预先提出这种思想将如何为您提供这些类型的界面设计,这为您提供了喘息的空间,可以在以后根据需要进行优化,而无需进行侵入性的设计更改。

多态性

多态性的经典示例也倾向于关注粒状一次一心态,例如Dog继承了Mammal。在有时可能导致开发人员开始不得不与类型系统对抗的瓶颈的游戏中,按子类型对多态基本指针进行排序以改善vtable上的临时位置,试图使数据成为通过自定义方式连续分配的特定子类型(例如Dog)分配器,以改善每个子类型实例的空间局部性,等等。

如果我们在较粗的层次上建模,这些负担都将不存在。您可以让Dogs继承抽象Mammals。现在,虚拟 dispatch 的成本已降低到每种类型的哺乳动物一次,而不是每种哺乳动物一次,并且可以有效且连续地代表特定类型的所有哺乳动物。

您仍然可以花很多精力,并以DOD思维方式利用OOP和多态性。诀窍是确保您在足够粗略的层次上设计事物,这样您就不会试图与类型系统抗争并解决数据类型以重新控制诸如内存布局之类的事物。如果您在足够粗略的层次上设计事物,则无需担心任何这些。

界面设计

就我所知,DOD仍然涉及接口(interface)设计,您可以在类中使用方法。设计适当的高级接口(interface)仍然非常重要,并且您仍然可以使用虚拟函数和模板并获得非常抽象的外观。我将重点关注的实际差异是,您可以像上面的adjust_brightness方法那样设计聚合接口(interface),这为您提供了优化的喘息空间,而无需在整个代码库中级联设计更改。我们设计了一种接口(interface)来处理整个图像的多个像素,而不是一次处理一个像素的接口(interface)。

DOD接口(interface)设计通常被设计为批量处理,并且通常以一种具有最佳存储布局的方式,用于必须访问所有内容的最关键性能,线性复杂性顺序循环。

因此,如果我们以Model为例,则缺少的是接口(interface)的聚合侧。
struct Models {
    // Methods to process models in bulk can go here.

    struct Model {
        // vertex buffers
        GLuint Positions, Normals, Texcoords, Elements;
        // textures
        GLuint Diffuse, Normal, Specular;
        // further material properties
        GLfloat Shininess;
    };

    std::vector<Model> models;
};

严格来说,这不必使用带有方法的类来表示。它可能是一个接受structs数组的函数。这些细节实际上并没有那么重要,重要的是,接口(interface)主要是为按顺序批量处理而设计的,而数据表示则针对这种情况进行了优化设计。

热/冷拆分

在查看Person类时,您可能仍会以一种经典的接口(interface)方式进行思考(即使此处的接口(interface)只是数据)。同样,DOD只会在整个性能上都使用struct,前提是它是最关键性能循环的最佳内存配置。这与人类的逻辑组织无关,而与机器的数据组织有关。
struct Person {
    Person() : Walking(false), Jumping(false) {}
    float Height, Mass;
    bool Walking, Jumping;
};

首先,我们将其放在上下文中:
struct People {
    struct Person {
        Person() : Walking(false), Jumping(false) {}
        float Height, Mass;
        bool Walking, Jumping;
     };
};

在这种情况下,是否经常将所有字段一起访问?假设,假设答案为否。这些WalkingJumping字段仅有时(冷)被访问,而HeightMass始终被重复访问(热)。在这种情况下,可能更理想的表示形式可能是:
struct People {
    vector<float> HeightMass;
    vector<bool> WalkingJumping;
};

当然,您可以在此处创建两个单独的结构,一个指向另一个,依此类推。关键是您最终要从内存布局/性能的 Angular 来设计此结构,理想情况下,您应该手头有一个良好的探查器并对扎实的结构有深入的了解通用的用户端代码路径。

从界面的 Angular 来看,您设计界面时要重点放在处理人员而非人员上。

问题

解决了这个问题:

I can only create models from this module, not from others. Should I move this to the type class Model while complexifying it?



这更多的是子系统设计方面的问题。由于您的Model rep全部与OpenGL数据有关,因此它可能应该属于可以正确初始化/销毁/渲染它的模块。它甚至可能是此模块的私有(private)/隐藏实现细节,此时,您将DOD思维方式应用于该模块的实现中。

但是,外部世界可以用来添加模型,销毁模型,渲染模型等的接口(interface)最终应该设计为批量的。可以将其视为为容器设计一个高级接口(interface),在该接口(interface)中,您很想为每个元素添加的方法最终都属于该容器,如上面的图像示例adjust_brightness所示。

复杂的初始化/销毁通常需要一次一次的设计思路,但是关键是您需要通过一个聚合接口(interface)来完成。在这里,您可能仍会放弃Model的标准构造函数和析构函数,而倾向于初始化添加GPU Model进行渲染,清除将GPU从列表中删除后的GPU资源。尽管您仍可以使用C++好东西来聚合接口(interface)(例如人),但仍可以返回单个类型(例如人)的C样式编码。

My question is, should I add methods to my type classes?



主要是为批量设计,您应该随便走。在您显示的示例中,通常没有。不一定是最困难的规则,但是您的类型正在为单个事物建模,并且为DOD留出空间通常需要缩小并设计处理许多事物的界面。

关于c++ - 类型应该在面向数据的设计中包含方法吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22127472/

相关文章:

c++ - 为什么我的返回 lambda 的函数似乎被转换为转换 int 的函数?

math - block 浮点运算与常规浮点运算

java - 无需获取/设置即可访问私有(private)属性(property)

asp.net-mvc - ASP.NET MVC 应用程序架构 "guidelines"

Django 应用程序中的 Ajax 架构

oop - 有哪些不同类型的封装?

c - C 中的内联 Setter 和 Getter 函数

c++ - 将 uint64 转换为 uint32

c++ - 如何索引指向数组[队列]的指针数组?

c++ - 在另一个文件中定义的静态常量数据成员