Должны ли типы иметь методы в проектировании, ориентированном на данные?

В настоящее время мое приложение состоит из трех типов классов. Он должен быть ориентирован на данные, поправьте меня, если это не так. Это три типа классов. Примеры кода не так важны, вы можете пропустить их, если хотите. Они нужны только для того, чтобы произвести впечатление. У меня вопрос: следует ли добавлять методы в классы типов?

Текущий дизайн

Типы просто хранят значения.

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 класс. Вышеупомянутый 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 для своих текстур и буферов вершин. Актуальные данные необходимо предварительно загрузить на видеокарту.

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

В настоящее время существует модуль Models с функцией Create(), которая занимается настройкой модели. Но таким образом я могу создавать модели только из этого модуля, а не из других. Должен ли я переместить это в класс типа Model при его усложнении? Я думал об определениях типов так же, как раньше.


person danijar    schedule 02.03.2014    source источник
comment
Вам следует прочитать этот сайт. Кажется, что ваш код не соответствует некоторым основным правилам, таким как обработка на основе существования (bool Walking, Jumping;). Вы используете unordered_map, когда DOD использует массивы (в основном везде). Использование классов, наследования и private полей также похоже на DOD. И нет, я думаю, вам не следует никуда добавлять методы, потому что методы означают, что в этом экземпляре нужно что-то делать !. В министерстве обороны вы думаете, что в некоторых случаях давайте сделаем что-то одно!   -  person cubuspl42    schedule 09.03.2014
comment
@ cubuspl42 Большое спасибо за ваш отзыв. Я спрошу о лучшей структуре данных в другом вопросе на этом сайте. Эти методы будут выполнять только такие задачи, как инициализация и очистка. В моем приложении эти экземпляры обычно создаются и уничтожаются по отдельности. Что вы думаете об этих задачах, должны они быть включены в тип или нет?   -  person danijar    schedule 09.03.2014
comment
Я так не думаю, но не уверен. Я не эксперт. Я просто программист, который в течение нескольких дней очень интересовался Министерством обороны США, и я прочитал почти все, что смог найти в Интернете. :) Самое сложное в DOD - это то, что он требует другого подхода к данным. Но только человек с определенным опытом может дать вам действительно ценную информацию. Удачи с вопросом о Министерстве обороны, это такая недооцененная и непопулярная вещь ...   -  person cubuspl42    schedule 09.03.2014


Ответы (1)


Во-первых, необязательно применять дизайн, ориентированный на данные, везде. В конечном итоге это оптимизация, и даже в критически важной для производительности кодовой базе по-прежнему есть множество частей, которые от этого не выигрывают.

Я часто думаю об этом как о стирающей структуре в пользу больших блоков данных, которые более эффективно обрабатывать. Возьмем, к примеру, изображение. Для эффективного представления своих пикселей обычно требуется хранить простой массив числовых значений, а не, скажем, коллекцию определенных пользователем абстрактных пиксельных объектов, которые в качестве преувеличенного примера имеют виртуальный указатель.

Представьте себе 4-компонентное (RGBA) 32-битное изображение, использующее числа с плавающей запятой, но использующее только 8-битную альфа-канал по какой-либо причине (извините, это своего рода глупый пример). Если бы мы даже использовали базовый 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-битных альфа-каналов уменьшает размер памяти и в результате потенциально повышает скорость последовательного доступа.

struct Image
{
    vector<float> rgb;
    vector<unsigned char> alpha;
};

... и именно так мы должны думать изначально: о данных, схемах памяти. Конечно, изображения обычно уже представлены эффективно, и алгоритмы обработки изображений уже реализованы для обработки большого количества пикселей в большом количестве.

Тем не менее, дизайн, ориентированный на данные, выводит это на более высокий уровень, чем обычно, применяя такое представление даже к вещам, которые находятся на значительно более высоком уровне, чем пиксель. Подобным образом вы можете получить выгоду от моделирования ParticleSystem вместо одного Particle, чтобы оставить передышку для оптимизации, или даже People вместо Person.

Но вернемся к примеру с изображением. Это может означать отсутствие Министерства обороны США:

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 метода в том, что он разработан с точки зрения интерфейса для работы с одним пикселем. Это может затруднить применение оптимизаций и алгоритмов, которые выигрывают от одновременного доступа к нескольким пикселям. Между тем, примерно так:

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

Полиморфизм

Классический пример полиморфизма также имеет тенденцию сосредотачиваться на этом гранулярном мышлении «одно за другим», например, Dog наследует Mammal. В играх, которые иногда могут приводить к возникновению узких мест, когда разработчикам приходится бороться с системой типов, сортировка полиморфных базовых указателей по подтипам для улучшения временной локализации в vtable, попытка сделать данные определенного подтипа (например, Dog), непрерывно распределенных с помощью пользовательских распределители для улучшения пространственной локализации для каждого экземпляра подтипа и т. д.

Ничего из этого не должно быть, если мы моделируем на более грубом уровне. Вы можете Dogs наследовать абстрактное Mammals. Теперь стоимость виртуальной отправки снижена до одного раза на тип млекопитающего, а не один раз на млекопитающее, и все млекопитающие определенного типа могут быть представлены эффективно и непрерывно.

Вы все еще можете использовать ООП и полиморфизм с мышлением Министерства обороны США. Хитрость заключается в том, чтобы убедиться, что вы проектируете вещи на достаточно грубом уровне, чтобы вы не пытались бороться с системой типов и обходить типы данных, чтобы восстановить контроль над такими вещами, как макеты памяти. Вам не придется возиться ни с чем из этого, если вы проектируете вещи на достаточно грубом уровне.

Дизайн интерфейса

Есть еще дизайн интерфейса, связанный с DOD, по крайней мере, насколько я понимаю, и вы можете иметь методы в своих классах. По-прежнему очень важно разрабатывать правильные высокоуровневые интерфейсы, и вы по-прежнему можете использовать виртуальные функции и шаблоны и работать очень абстрактно. Практическое отличие, на котором я хотел бы сосредоточиться, заключается в том, что вы разрабатываете агрегированные интерфейсы, как в случае adjust_brightness метода выше, что оставляет вам передышку для оптимизации без каскадных изменений дизайна во всей кодовой базе. Мы разрабатываем интерфейс для обработки нескольких пикселей всего изображения вместо одного, который обрабатывает один пиксель за раз.

Проекты интерфейсов DOD часто предназначены для массовой обработки и, как правило, с оптимальной компоновкой памяти для наиболее критичных к производительности последовательных циклов линейной сложности, которые должны иметь доступ ко всему.

Итак, если мы возьмем ваш пример с Model, чего не хватает, так это агрегированной стороны интерфейса.

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

Горячее / холодное разделение

Глядя на ваш Person класс, вы все еще можете думать в некотором роде в стиле классического интерфейса (хотя интерфейс здесь - это просто данные). Опять же, 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;
     };
};

В этом случае часто ли обращаются ко всем полям вместе? Предположим, гипотетически ответ отрицательный. Эти поля Walking и Jumping доступны только иногда (холодно), в то время как к Height и Mass обращаются постоянно (горячо). В этом случае потенциально более оптимальным представлением может быть:

struct People {
    vector<float> HeightMass;
    vector<bool> WalkingJumping;
};

Конечно, вы можете создать здесь две отдельные структуры, соединить одну точку с другой и т. Д. Ключ в том, что вы разрабатываете это в конечном итоге с точки зрения макета памяти / производительности, а в идеале с хорошим профилировщиком в руках и твердым пониманием общие пути кода на стороне пользователя.

С точки зрения интерфейса, вы разрабатываете интерфейс с упором на обработку людей, а не человека.

Проблема

Разобравшись с этим, перейдем к вашей проблеме:

Я могу создавать модели только из этого модуля, а не из других. Должен ли я переместить это в класс типа Model при его усложнении?

Это скорее проблема дизайна подсистемы. Поскольку ваш Model представитель полностью посвящен данным OpenGL, он, вероятно, должен принадлежать модулю, который может правильно инициализировать / уничтожить / отобразить его. Это может быть даже частная / скрытая деталь реализации этого модуля, и в этот момент вы применяете мышление Министерства обороны США в рамках реализации модуля.

Однако интерфейс, доступный внешнему миру для добавления моделей, уничтожения моделей, их рендеринга и т. Д., В конечном итоге должен быть разработан для массового использования. Думайте об этом как о разработке высокоуровневого интерфейса для контейнера, в котором методы, которые вы можете добавить для каждого элемента, вместо этого будут принадлежать контейнеру, как в нашем примере изображения выше с adjust_brightness.

Сложная инициализация / уничтожение часто требует индивидуального подхода к проектированию, но главное в том, что вы делаете это через агрегированный интерфейс. Здесь вы все равно можете отказаться от стандартного конструктора и деструктора для Model в пользу инициализации при добавлении Model GPU для рендеринга, очистки ресурсов GPU при удалении его из списка. Это отчасти возвращается к кодированию в стиле C для отдельного типа (например, человека), хотя вы все равно можете получить очень изощренные возможности с помощью C ++ для совокупного интерфейса (например, людей).

У меня вопрос: следует ли добавлять методы в классы типов?

В основном дизайн для больших объемов, и вы должны быть в пути. В приведенных вами примерах обычно нет. Это не должно быть самым сложным правилом, но ваши типы моделируют отдельные вещи, и для того, чтобы оставить место для Министерства обороны США, часто требуется уменьшение масштаба и разработка интерфейсов, которые имеют дело со многими вещами.

person Community    schedule 29.11.2015
comment
Очень хорошее описание! Не ожидал этого через полтора года. - person danijar; 29.11.2015
comment
@danijar Надеюсь, что срок годности не истек, и это может как-то помочь. Я копался в старых вопросах и ответах, пытаясь найти те, которые могут дать мне повод написать о том, с чем я часто имею дело. - person ; 29.11.2015