Подтвердить что ты не робот

Хранят многие отношения 1:1 между различными типами объектов: развязка и высокая производительность

У меня есть более 300 классов. Они связаны в некотором роде.

Для простоты все отношения равны 1:1. Вот примерная диаграмма.

введите описание изображения здесь (В реальном случае их около 50 -отношения пар.)

Примечание.. В некоторых случаях может существовать некоторая связь.
Например, некоторые hen не относятся к какому-либо food.

Примечание2: Нет ссылки = никогда, например. каждый egg не относится ни к какому cage.
Такое отношение никогда не будет добавлено/удалено/запрошено.

Вопрос:

Как хранить отношения между ними элегантно?
Кажется, что у всех 4 моих идей (ниже) есть недостатки.

Здесь - связанный вопрос, но с отношением 1: N и только 1.

Мои плохие решения

Это полу-псевдокоды.

Версия 1 Прямая

Моя первая мысль - добавить указатели друг к другу.

Chick.h: -

class Egg;
class Food;
class Chick{  Egg* egg; Food* food;}

Hen.h: -

class Egg; class Cage; class Food;
class Hen{ Egg* egg; Cage* cage; Food* food;}

Очень дешево добавлять/удалять отношение и запрос, например.: -

int main(){
    Hen* hen;    ...    Egg* egg=hen->egg;
}

Это работает хорошо, но по мере того, как моя программа растет, я хочу отделить их.
Грубо говоря, Hen.h не должно содержать слова egg и наоборот.

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

Версия 2 Hash-map

Используйте std::unordered_map.
Это будет шея бутылки моей программы. (профилированный в режиме выпуска)

class Egg{}; class Hen{};  //empty (nice)
.....
int main(){
    std::unordered_map<Hen*,Egg*> henToEgg;
    std::unordered_map<Egg*,Hen*> eggToHen;
    ....
    Hen* hen;    ...    Egg* egg=henToEgg[hen];
}

Версия 3 Посредник-одиночный

Храните все отношения в одном большом медиаторе для каждого объекта.
Извлеките большую память для пустых слотов (например, egg имеет слот henFood_hen).
Total waste = type-of-relation-pair * 2 * 4 байта (если выполняется в 32 бита) в каждом объекте.

class Mediator {
    Egg* eggHen_egg=nullptr;
    Hen* eggHen_hen=nullptr;
    Hen* henFood_hen=nullptr;
    Food* henFood_food=nullptr;
    //... no of line = relation * 2
};
class Base{public: Mediator m;};
class Egg : public Base{};  //empty (nice)
class Hen : public Base{}; 
int main(){
     Hen* hen;    ...    Egg* egg=hen->eggHen_egg;
}

Версия 4 Посредник-массив (аналогично 3)

Попробуйте стандартизировать - высокая гибкость.

class Mediator {
    Base* ptrLeft[5];
    Base* ptrRight[5];
};
class Base{public: Mediator m;};
class Egg : public Base{};  //empty (nice)
class Hen : public Base{}; 
int main(){
     enum RELA_X{RELA_HEN_EGG,RELA_HEN_CAGE,RELA_EGG_CHICK, .... };
     Hen* hen;    ...    
     Egg* egg=hen->m.ptrRight[RELA_HEN_EGG]; 
     //^ get right of "hen-egg" === get "egg" from "hen"
     //^ can be encapsulated for more awesome calling
}

Плюсы и минусы

Зеленый (+) хорош. Красный (-) плох. введите описание изображения здесь

Изменить: Я использую Entity-Component для игры 60fps.
Это постоянная база данных: один экземпляр, используемый для всей жизни игры.

Edit2:. Все отношения - это слабое отношение, а не свойство a-a или strong std::unique_ptr. (Спасибо Уолтеру)

  • A hen находится в a cage.
    Некоторые hens не находятся ни в одном cage, а некоторые cages пустые.
  • A chick исходит от a egg.
    Однако некоторые chicks не исходили ни от каких egg (они просто удалены с неба),
    и некоторым eggs не повезло стать chick.
  • A hen и a chick едят a (вероятно, такую ​​же) пластину food.
    Некоторые пластины food только что приготовлены, но не поданы.

Edit3: Назначение целочисленного идентификатора для каждого объекта может быть хорошей идеей.
(Спасибо Оливу, ахокса и Симоне Чифани)

Edit4:: Не нужно предоставлять компилируемый код, достаточно существенной части/концепции.

4b9b3361

Ответ 1

В соответствии с требованиями, если у вас есть отношения "один-к-одному", это звучит для меня как график. В этом случае, если он плотно заселен (существует много отношений), я бы использовал матричное представление графика. В приведенных ниже таблицах я связывал числа от 0 до 4 с сущностями (курица, клетка, еда, яйцо и цыпленок) соответственно. Если существует соотношение Hen-Egg, то матрица будет иметь 1 в позиции matrix[0][3], если она не будет тогда, значение будет 0 (вы можете выбрать значения по вашему выбору, чтобы решить, как определить, когда существует отношение или нет). Если отношения неориентированы, вам нужна только одна сторона матрицы (например, верхний треугольник).

+---------------------------------+
| Hen | Cage | Food | Egg | Chick |
+---------------------------------+
|  0  |  1   |  2   |  3  |   4   |
+---------------------------------+

      0   1   2   3   4
    +--------------------+
  0 | 0 | 1 | 0 | 1 | 1  |
    +---+---+---+---+----+
  1 | 0 | 0 | 0 | 1 | 1  |
    +---+---+---+---+----+
  2 | 0 | 0 | 0 | 0 | 1  |
    +---+---+---+---+----+
  3 | 0 | 0 | 0 | 0 | 1  |
    +---+---+---+---+----+
  4 | 0 | 0 | 0 | 0 | 0  |
    +--------------------+

Недостаток этого решения скрывается в использовании памяти, особенно если в матрице содержится много 0 (отношений, которые не существуют); вам будет излишне занимать много места. В этом случае вы можете использовать связанный список графиков.

Ответ 2

Мое предложение:

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

class Base
{
   public:
      virtual ~Base();

      // Add "child" to the list of children of "this"
      // Add "this" to the list of parents of "child"
      void addChild(Base* child);

      // Remove "child" from the list of children of "this"
      // Remove "this" from the list of parents of "child"
      void removeChild(Base* child);                                 

      std::vector<Base*>& getParents();
      std::vector<Base*> const& getParents() const;

      std::vector<Base*>& getChildren();
      std::vector<Base*> const& getChildren() const;

   private:
    std::vector<Base*> parents_;
    std::vector<Base*> chilren_;    
};

Теперь вы можете реализовать функции более высокого уровня. Например.

// Call function fun() for each child of type T of object b.
template <typename T>
void forEachChild(Base& b, void (*fun)(T&))
{
   for ( auto child, b.getChildren() )
   {
      T* ptr = dynamic_cast<T*>(child);
      if ( ptr )
      {
         fun(*ptr);
      }
   }
}

Чтобы запросить уникальный egg из hen, вы можете использовать шаблон общей функции.

template <typename T>
T* getUniqueChild(Base& b)
{
   T* child = nullptr;
   for ( auto child, b.getChildren() )
   {
      T* ptr = dynamic_cast<T*>(child);
      if ( ptr )
      {
         if ( child )
         {
            // Found at least two.
            // Print a message, if necessary.
            return NULL;
         }

         child = ptr;
      }
   }

   return child;
}

а затем используйте его как:

hen* henptr = <get a pointer to a hen object>;
egg* eggptr = getUniqueChild<egg>(*henptr);

Ответ 3

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

1) типичное отношение is-a, например. ваш случай egg -> food. Похоже, что это простое наследование, когда класс Egg должен быть выведен из Food class

2), т.е. hen -> egg кейс. Здесь вы знаете, что каждый hen может иметь (производить?) Один или несколько Egg s, это часть вашей игровой логики, и эта информация заслуживает жесткого кодирования, для удобства, удобочитаемости и производительности: например, hen.eggs.count(). В этом случае вы знаете, какой тип (почти конкретный) ожидается, поэтому объявление выглядит как

class Hen:
    List<Egg> eggs;

Я не уверен, что развязка здесь выгодна, поскольку использовать eggs вам нужно знать о классе Egg.

3) абстрактные компоненты. На абстрактном уровне игрового движка, когда у вас нет какой-либо конкретной логики игры (или вы не хотите ее использовать). Например. Unity3D Component s или Unreal Engine Actor s. Их основная цель - помочь вам организовать ваши вещи в иерархии, чтобы вы могли клонировать часть вашего игрового мира (например, сложное здание, состоящее из множества частей), перемещать его, реорганизовывать и т.д. У вас есть базовый класс для этих компонентов, и вы может перечислить дочерние компоненты или запросить конкретного ребенка по его имени или некоторому идентификатору. Этот метод является абстрактным и помогает отделить логику игрового движка от конкретной игровой логики. Это не означает, что он применим только для повторно используемых игровых движков. Даже игры, которые были построены с нуля, не используя сторонние игровые движки, как правило, имеют некоторую логику "игрового движка". Обычно такая модель компонента включает в себя некоторые накладные расходы, например. cage.get_all_components("hen").count() - гораздо более типизированное, менее читаемое и есть некоторые служебные данные времени выполнения, чтобы перечислять только hen и считать их.

class Component:
    List<Component> children;

Как вы можете видеть здесь, у вас нет зависимостей между классами, которые происходят из Component. Поэтому в идеале, имея дело с children, вам не нужно знать их конкретный тип, а abstract Component достаточно, чтобы делать общие вещи, например, указать место в игровом мире, удалить или переименовать его. Хотя на практике принято бросать его на конкретный тип, поэтому развязка здесь - это просто отделить логику игрового движка от логики игры.

В порядке, чтобы объединить все три метода.

Ответ 4

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

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

Если ваш алгоритм должен работать над отношением, между кур и яйцами. Они должны быть местными. Этого не может быть достигнуто путем сохранения их в качестве указателей на кур, но вам нужен массив всех зависимостей между курицей и яйцом.

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

Ответ 5

Я последовал подходу, подобному методу R Sahu, для создания библиотеки необработанного персистентности. В моей реализации каждый объект должен реализовать базовый интерфейс, называемый IEntity. Объект в основном содержит вектор полей, представленный интерфейсом IField, например:

typedef shared_ptr<IField>  field_ptr;
typedef vector<field_ptr> fields_vec;

class IEntity
{
public: 

    virtual string const& getEntityName() = 0;
    virtual bool allowDuplicates() = 0;
    virtual fields_vec const& getFields() = 0;
    virtual void setFieldValue(string fieldName, string fieldValue) = 0;
    //callback is called after queries to fill the queryResult map (fieldName, fieldValue)
    virtual void callback(map<string, string> queryResult) = 0;
};

class IField
{
public:
    typedef enum
    {
        INTEGER,
        FLOAT,
        REAL,
        NUMERIC,
        DATE,
        TIME,
        TIMESTAMP,
        VARCHAR
    } Type;

    virtual string const&  getName()              const = 0;
    virtual Type           getType()              const = 0;
    virtual string const&  getValue()             const = 0;    
    virtual bool           isPrimaryKey()         const = 0;
    virtual bool           isForeignKey()         const = 0;
    virtual bool           isUnique()             const = 0;
    virtual bool           isAutoIncrement()      const = 0;
    virtual bool           isNotNull()            const = 0;
    virtual int            getVarcharSize()       const = 0;
    virtual void           setValue(string value)       = 0;
    // Manage relations
    virtual IEntity* const getReferenceEntity()   const = 0;
    virtual string const&  getReferenceField()    const = 0; 
};

class CField : 
    public IField
{
public:
    CField(string name, Type type, bool primaryKey, bool unique, bool autoincrement, 
        bool notNull = false, int varcharSize = 0)
    {
        ...
    }

    CField(string name, Type type, IEntity* const referenceEntity, string const& referenceField,
        bool notNull = false, int varcharSize = 0)
    {
        ...
    }

    ...
};

Затем у меня есть менеджер объектов, который предоставляет основные функции сохранения:

class CEntityManager
{
public:
    CEntityManager();
    virtual ~CEntityManager();

    //--------------------------------------------//
    //Initializes db and creates tables if they not exist 
    bool initialize(string sDbName, vector<shared_ptr<IEntity>> const& entities);

    //--------------------------------------------//
    //Returns a shared_ptr instance of IField   
    field_ptr createField(string name, IField::Type type,
        bool primaryKey = false, bool unique = false, bool autoincrement = false, bool notNull = false, int varcharSize = 0);

    //--------------------------------------------//
    //Returns a shared_ptr instance of IField, 
    //When the field represents a foreign key, 'referenceField' specifies the column referenced to the 'referenceEntity'
    // and 'updateBy' specifies the column of the referenceEntity to check for update.
    field_ptr createField(string name, IField::Type type,
        IEntity* const referenceEntity, string referenceField, string updateBy, bool notNull = false, int varcharSize = 0);

    //--------------------------------------------//
    //Begin a new transaction
    void beginTransaction();

    //--------------------------------------------//
    //Commit query to database
    bool commit();

    //--------------------------------------------//
    //Persists an entity instance to db
    void persist(IEntity * const entity);

    //--------------------------------------------//
    template <class T>
    vector<shared_ptr<T>> find(vector<WhereClause> restrictions)

    //--------------------------------------------//
    //Removes one or more entities given the specified conditions
    void remove(string const& entityName, vector<WhereClause> restrictions);
};

class WhereClause
{
public:
    typedef enum
    {
        EQUAL,
        NOT_EQUAL,
        GREATER_THAN,
        LESS_THAN,
        GREATER_THAN_OR_EQUAL,
        LESS_THAN_OR_EQUAL,
        BETWEEN,
        LIKE,
        IN_RANGE
    } Operator;

    string fieldName;
    string fieldValue;
    Operator op;
};

PROs этого решения - многократное использование, высокий уровень абстракции и легкость изменения механизма БД.
CONs - это то, что он будет медленнее по отношению к прямому решению
Однако я использую его с sqlite на db тысячи записей со временем ответа в диапазоне от 100 до 600 мс, что приемлемо для меня.

В вашем случае у вас будет что-то вроде:

class Egg: 
    public IEntity
{
public:
    Egg()
    {
        m_fields.push_back(shared_ptr<CField>(new CField("Id", IField::INTEGER, ...));
        // add fields
    }

private:
    fields_vec m_fields;
};

class Hen : 
    public IEntity
{
public:
    Hen()
    {
        m_fields.push_back(shared_ptr<CField>(new CField("Id", IField::INTEGER, ...));
        // add fields

        //here we add a field which represent a reference to an Egg record through the field 'Id' of Egg entity
        m_fields.push_back(shared_ptr<CField>(new CField("EggId", IField::INTEGER, dynamic_cast<IEntity*> (m_egg.get()), string("Id")));
    }

private:
    fields_vec m_fields;
    unique_ptr<Egg> m_egg;        
};

Затем вы можете получить свою запись Хен, содержащую ссылку на Egg, из EntityManager

vector<WhereClause> restrictions;
restrictions.push_back(WhereClause("Id", idToFind, EQUALS));

vector<shared_ptr<Hen>> vec = m_entityManager->find<Hen>(restrictions);

Этот пример представляет соотношение 1:1 между Куриным и Яйцо. Для отношения 1: N вы можете инвертировать представление и поместить ссылку Hen в Egg

Ответ 6

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

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

Я использую 2 класса, которые получены из общего базового интерфейса для моего примера, и класс Primary назван для представления класса, у которого есть отношение, назначенное для того, где ассоциированный класс является классом, является делегированным классом, который является назначается, чтобы дать первому классу ссылку на связь.

class Base {
protected:
    std::vector<std::string> vRelationshipNames_;
    std::vector<std::shared_ptr<Base> vRelationships_;

public:
    Base(){}
    virtual ~Base(){}
    virtual void updateListOfNames( std::vector<std::string> newNames );
};

class Primary : Base {
private:
    std::string objectName_;
public:
    // Constructor if relationships are not known at time of instantiation.
    explicit Primary( const std::string& name );         
    // Constructor if some or all relationships are known. If more are discovered then the update function can be used.
    Primary( const std::string& name, std::vector<std::string> relationshipNames );

    // Add by const reference
    void add( const Base& obj );

    // Remove by const reference or by string name.
    void remove( const Base& obj );
    void remove( const std::string& name );

    // If needed you can even override the update method.
    virtual void updateListOfNames( std::vector<std::string> newNames ) override;
};

// Would basically have similar fields and methods as the class above, stripped them out for simplicity.
class Associate : Base {
    std::string objectName_;
};

Затем мы можем использовать шаблон функции, который принимает два объекта класса для поиска, чтобы увидеть, является ли ассоциированный объект в списке имен основного объекта

template <class T, class U>
T& setRelationshipBetweenClasses( class T& primaryObject, class U& associateObject ) {
    // Search through primaryObject list of names to see if associate class is listed
    // If it is not then return from function otherwise, we need to search to see
    // if this class was already added to its list of shared pointers. 

    // If it is not found then add it by calling the Primary add function

    // Then we also need to call the Associates add function as well by 
    // passing it a const reference to the Primary class this way both
    // classes now have that relationship. 

    // we also return back the reference of the changed Primary object.      
}

ИЗМЕНИТЬ

OP сделал комментарий об использовании строки и медленности; Я использовал строку здесь в псевдокоде просто для ясности понимания, вы можете заменить std::string на unsigned int и просто использовать числовой идентификатор. Он будет делать то же самое и должен быть достаточно эффективным.

ИЗМЕНИТЬ

Для OP -

Общий интерфейс классов без определений и реализаций, но их объявления могут выглядеть примерно так:

Example.h

#ifndef EXAMPLE_H
#define EXAMPLE_H

struct CommonProperties {
    std::string name_;
    unsigned int id_;

    // Default
    explicit CommonProperties() : name_(std::string()), id_(counter_) {
        counter_++;
    }
    // Passed In Name
    explicit CommonProperties(const std::string& name) : name_(name), id_(counter_) {
        counter_++;
    }

private:
    static unsigned int counter_;
};


class BaseObject {
protected:
    CommonProperties properties_;
                                                             // Sizes of Both Containers Should Always Match!
    std::vector<std::shared_ptr<BaseObject>> sharedObjects_; // Container of Shared Names
    std::vector<unsigned int> sharedObjectIDs_;              // Container of Shared IDs

public:
    explicit BaseObject(const std::string& strName) {
        properties_.name_ = strName;
    }

    // Virtual Interface for Abstract Base Class    
    virtual void add(const BaseObject& obj, const std::string& strName, const unsigned int id) = 0; // Purely Virtual Each Derived Class Must Implement
    virtual void update(const BaseObject& obj, const std::string& strName, const unsigned int id) = 0; // Also purely virtual
    virtual void remove(const std::string& strName) {} // Used string method to remove
    virtual void remove(const unsigned int id) {} // Use ID method to remove

    // Get Containers
    std::vector<std::shared_ptr<BaseObject>> getObjects() const { return sharedObjects_; }
    std::vector<unsigned int> getIDs() const { return sharedObjectIDs_; }
};


class Primary : public BaseObject {
// Member Variables
public:
protected:
private:

// Constructors, Destructor and Methods or Functions
public:
    explicit Primary(const std::string& strName) : BaseObject(strName) {
    }

    // Must Have Purely Virtual
    void add(const BaseObject& obj, const std::string& strName, const unsigned int id) override {
        // Algorithm Here
    }

    void update(const BaseObject& obj, const std::string& strName, const unsigned int id) override {
        // Algorithm Here
    }

    // other public methods;
protected:
private:
};

class Associate : public BaseObject {
    // Member Variables:
public:
protected:
private:

    // Constructors, Destructors and Methods or Functions
public:
    explicit Associate(const std::string& strName) : BaseObject(strName) {
    }

    // Must Have Purely Virtual
    void add(const BaseObject& obj, const std::string& strName, const unsigned int id) override {
        // Algorithm Here
    }

    void update(const BaseObject& obj, const std::string& strName, const unsigned int id) override {
        // Algorithm Here
    }

protected:
private:

};    

#endif // EXAMPLE_H

Example.cpp

#include "stdafx.h"  // Used for common std containers and algorithms as well as OS and system file includes.
#include "Example.h"

unsigned int CommonProperties::counter_ = 0x00;

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

Например, скажем, я создал 3 разных объекта, которые имеют разные классы: class1, class2, class3 их имена и идентификаторы устанавливаются при создании. Я не показывал, как автоматически создавать уникальную строку с базовым набором символов для определенного класса, а затем добавлять к этой строке уникальное значение каждый раз при создании экземпляра класса, но это то, что я обычно делаю, если имя не является в комплект поставки. Поставка имени является необязательной, и генерация имен обычно автоматическая. Таблица разных классов и поля имени свойств будут выглядеть следующим образом:

// CLASS NAME  |   ID
    "class1"   |   0x01
    "class2"   |   0x02
    "class3"   |   0x03

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

class PickupTruck {};

// Table of Names & IDS similar to above:
   "Chevy"     |  0x04
   "Dodge"     |  0x05
   "Ford"      |  0x06
   "GMC"       |  0x07

Теперь, если вы хотите провести различие между именем фактического класса и именем фактического описания объекта; просто убедитесь, что вы добавили std::string в качестве защищенного члена в класс Base или Super, из которого происходят эти классы. Таким образом, это имя будет представлять строковое представление этого типа класса, где имя листа свойств будет фактическим описательным именем этого объекта. Но при выполнении фактических поисков и удалений из ваших контейнеров, используя идентификаторы для простых циклов, счетчики и индексирование являются довольно эффективными.