Entity collections
To implement entity collections, we need to leverage some more advanced C++ techniques, and we will take a brief break from our conventions so far, implementing multiple classes in a single header file.
Create entity-collection.h in cm-lib/source/data, and in it, add our namespaces as normal and forward declare Entity:
#ifndef ENTITYCOLLECTION_H #define ENTITYCOLLECTION_H
namespace cm { namespace data { class Entity;
}}
#endif
Next, we’ll walk through the necessary classes in turn, each of which must be added in order inside the namespaces.
We first define the root class, which does nothing more than inheriting from QObject and giving us access to all the goodness that it brings, such as object ownership and signals. This is required because classes deriving directly from QObject cannot be templated:
class CMLIBSHARED_EXPORT EntityCollectionObject : public QObject { Q_OBJECT
public: EntityCollectionObject(QObject* _parent = nullptr) : QObject(_parent) {} virtual ~EntityCollectionObject() {}
signals: void collectionChanged(); };
You will need to add includes for QObject and our DLL export macros. Next, we need a type agnostic interface to use with our entities, much the same as we have with the DataDecorator and Entity maps we’ve implemented. However, things are a little more complicated here, as we will not derive a new class for each collection we have, so we need some way of getting typed data. We have two requirements. Firstly, the UI needs a QList of derived types (for example, Client*) so that it can access all the properties specific to a client and display all the data. Secondly, our Entity class needs a vector of base types (Entity*) so that it can iterate its collections without caring exactly which type it is dealing with. The way we achieve this is to declare two template methods but delay defining them until later. derivedEntities() will be used when the consumer wants a collection of the derived type, while baseEntities() will be used when the consumer just wants access to the base interface:
class EntityCollectionBase : public EntityCollectionObject { public: EntityCollectionBase(QObject* parent = nullptr, const QString& key
= "SomeCollectionKey") : EntityCollectionObject(parent) , key(key) {} virtual ~EntityCollectionBase() {}
QString getKey() const { return key; }
virtual void clear() = 0; virtual void update(const QJsonArray& json) = 0; virtual std::vector<Entity*> baseEntities() = 0;
template <class T> QList<T*>& derivedEntities();
template <class T> T* addEntity(T* entity);
private: QString key; };
Next, we declare a full template class where we store our collection of derived types and implement all of our methods, except for the two template methods we just discussed:
template <typename T> class EntityCollection : public EntityCollectionBase { public: EntityCollection(QObject* parent = nullptr, const QString& key =
"SomeCollectionKey") : EntityCollectionBase(parent, key) {} ~EntityCollection() {}
void clear() override { for(auto entity : collection) { entity->deleteLater(); } collection.clear(); }
void update(const QJsonArray& jsonArray) override { clear(); for(const QJsonValue& jsonValue : jsonArray) { addEntity(new T(this, jsonValue.toObject())); } }
std::vector<Entity*> baseEntities() override { std::vector<Entity*> returnValue; for(T* entity : collection) { returnValue.push_back(entity); } return returnValue; }
QList<T*>& derivedEntities() { return collection; }
T* addEntity(T* entity) { if(!collection.contains(entity)) { collection.append(entity); EntityCollectionObject::collectionChanged(); } return entity; }
private: QList<T*> collection; };
You will need #include <QJsonValue> and <QJsonArray> for these classes.
The clear() method simply empties the collection and tidies up the memory; update() is conceptually the same as the JSON methods we implemented in Entity, except that we are dealing with a collection of entities, so we take a JSON array instead of an object. addEntity() adds an instance of a derived class to the collection, and derivedEntities() returns the collection; baseEntities() does a little more work, creating a new vector on request and populating it with all the items in the collection. It is just implicitly casting pointers, so we’re not concerned about expensive object instantiation.
Finally, we provide the implementation for our magic templated methods:
template <class T> QList<T*>& EntityCollectionBase::derivedEntities() { return dynamic_cast<const EntityCollection<T>&>(*this).derivedEntities(); }
template <class T> T* EntityCollectionBase::addEntity(T* entity) { return dynamic_cast<const EntityCollection<T>&>(*this).addEntity(entity); }
What we’ve achieved by delaying our implementation of these methods is that we’ve now fully declared our templated EntityCollection class. We can now "route" any calls to the templated methods through to the implementation in the templated class. It’s a tricky technique to wrap your head around, but it will hopefully make more sense when we start implementing these collections in our real-world models.
With our entity collections now ready, we can return to our Entity class and add them to the mix.
In the header, #include <data/entity-collection.h>, add the signal:
void childCollectionsChanged(const QString& collectionKey);
Also, add the protected method:
EntityCollectionBase* addChildCollection(EntityCollectionBase* entityCollection);
In the implementation file, add the private member:
std::map<QString, EntityCollectionBase*> childCollections;
Then, add the method:
EntityCollectionBase* Entity::addChildCollection(EntityCollectionBase* entityCollection) { if(implementation->childCollections.find(entityCollection-
>getKey()) == std::end(implementation->childCollections)) { implementation->childCollections[entityCollection->getKey()] =
entityCollection; emit childCollectionsChanged(entityCollection->getKey()); } return entityCollection; }
This works in exactly the same way as the other maps, associating a key with a pointer to a base class.
Next, add the collections to the update() method:
void Entity::update(const QJsonObject& jsonObject) { // Update data decorators for (std::pair<QString, DataDecorator*> dataDecoratorPair :
implementation->dataDecorators) { dataDecoratorPair.second->update(jsonObject); }
// Update child entities for (std::pair<QString, Entity*> childEntityPair : implementation-
>childEntities) { childEntityPair.second-
>update(jsonObject.value(childEntityPair.first).toObject()); }
// Update child collections for (std::pair<QString, EntityCollectionBase*> childCollectionPair
: implementation->childCollections) { childCollectionPair.second-
>update(jsonObject.value(childCollectionPair.first).toArray()); } }
Finally, add the collections to the toJson() method:
QJsonObject Entity::toJson() const { QJsonObject returnValue;
// Add data decorators for (std::pair<QString, DataDecorator*> dataDecoratorPair :
implementation->dataDecorators) { returnValue.insert( dataDecoratorPair.first,
dataDecoratorPair.second->jsonValue() ); }
// Add child entities for (std::pair<QString, Entity*> childEntityPair : implementation-
>childEntities) { returnValue.insert( childEntityPair.first,
childEntityPair.second->toJson() ); }
// Add child collections for (std::pair<QString, EntityCollectionBase*> childCollectionPair
: implementation->childCollections) { QJsonArray entityArray; for (Entity* entity : childCollectionPair.second-
>baseEntities()) { entityArray.append( entity->toJson() ); } returnValue.insert( childCollectionPair.first, entityArray ); }
return returnValue; }
You will need #include <QJsonArray> for that last snippet.
We use the baseEntities() method to give us a collection of Entity*. We then append the JSON object from each entity to a JSON array and when complete, add that array to our root JSON object with the collection’s key.
The past few sections have been quite long and complex and may seem like a lot of work just to implement some data models. However, it’s all code that you write once, and it gives you a lot of functionality for free with every entity you go on and make, so it’s worth the investment in the long run. We’ll go ahead and look at how to implement these classes in our data models.