Трябва ли типовете да имат методи в дизайна, ориентиран към данни?

В момента моето приложение се състои от три вида класове. Трябва да следва дизайн, ориентиран към данни, моля, поправете ме, ако не е така. Това са трите вида класове. Примерите за кодове не са толкова важни, можете да ги пропуснете, ако искате. Те са там само за да оставят впечатление. Въпросът ми е трябва ли да добавя методи към моите типови класове?

Актуален дизайн

Типовете просто съхраняват стойности.

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. И не, мисля, че не трябва да добавяте методи никъде, защото методите означават да правите някои неща в този екземпляр!. В DOD мислите, че нека направим едно нещо в някои случаи!.   -  person cubuspl42    schedule 09.03.2014
comment
@cubuspl42 Благодаря много за обратната връзка. Ще попитам за по-добра структура на данните в друг въпрос на този сайт. Методите ще изпълняват само задачи като инициализация и почистване. В моето приложение тези екземпляри обикновено се конструират и унищожават индивидуално. Какво мислите за тези задачи, трябва ли да бъдат включени в типа или не?   -  person danijar    schedule 09.03.2014
comment
Не мисля така, но не съм сигурен. Аз не съм експерт. Аз съм просто програмист, който се интересува много от DOD от няколко дни и прочетох почти всичко, което можах да намеря в интернет. :) Най-трудното при DOD е, че изисква друг начин на мислене за данните. Но само човек с известен опит може да ви даде наистина ценна информация. Успех да питаш за 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.

Но да се върнем към примера с изображение. Това би означавало липса на 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 метод е, че той е проектиран от гледна точка на интерфейса да работи върху един пиксел. Това може да затрудни прилагането на оптимизации и алгоритми, които се възползват от достъпа до множество пиксели наведнъж. Междувременно, нещо като това:

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. Номерът е да се уверите, че проектирате нещата на достатъчно грубо ниво, така че да не се опитвате да се борите срещу системата от типове и да работите около типовете данни, за да си възвърнете контрола върху неща като оформление на паметта. Няма да се налага да се занимавате с нищо от това, ако проектирате нещата на достатъчно грубо ниво.

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

Все още има дизайн на интерфейс, включен в 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 rep е изцяло за OpenGL данни, той вероятно трябва да принадлежи към модула, който може правилно да го инициализира/унищожи/рендерира. Може дори да е частен/скрит детайл за изпълнение на този модул, в който момент прилагате DOD начин на мислене в изпълнението на модула.

Интерфейсът, достъпен за външния свят за добавяне на модели, унищожаване на модели, изобразяване и т.н., обаче, в крайна сметка трябва да бъде проектиран за масово. Мислете за това като за проектиране на интерфейс на високо ниво за контейнер, където методите, които бихте се изкушили да добавите за всеки елемент, вместо това в крайна сметка принадлежат на контейнера, както в нашия пример с изображение по-горе с adjust_brightness.

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

Въпросът ми е трябва ли да добавя методи към моите типови класове?

Основно дизайн за насипно състояние и трябва да сте на път. В примерите, които показахте, обикновено не. Не е нужно да е най-трудното правило, но вашите типове моделират отделни неща и за да оставите място за DOD често се налага намаляване на мащаба и проектиране на интерфейси, които се занимават с много неща.

person Community    schedule 29.11.2015
comment
Много добро писане! Не очаквах това след година и половина. - person danijar; 29.11.2015
comment
@danijar Надявам се, че не е изтекъл срокът на годност, където може да помогне по някакъв начин. Рових се в стари Въпроси и отговори, опитвайки се да намеря такива, които могат да ми дадат извинение да пиша за нещо, с което се занимавам често. - person ; 29.11.2015