QSerializer: solution pour une sérialisation JSON / XML simple

Bonjour, Habr!

Je pensais que cela se révélait injustement - en Java, C #, Go, Python, etc. Il existe des bibliothèques pour une sérialisation confortable des données d'objet en JSON et XML désormais à la mode, mais en C ++, ils ont oublié, ou ne voulaient pas, ou n'en avaient pas vraiment besoin, ou si tout cela est compliqué, ou peut-être tous ensemble. J'ai donc décidé de réparer ce truc.

Tous les détails, comme d'habitude, sous la coupe.

image

Contexte


Une fois de plus, j'ai décidé de reprendre le prochain projet pour animaux de compagnie, dont l'essence était l'échange client-serveur, tandis que le serveur préféré de beaucoup était RaspberryPi. Entre autres choses, je m'intéressais à la question de la création de «points de sauvegarde» - afin que je puisse le plus simplement possible, dans le cadre du prototype, enregistrer l'état de l'objet avant de quitter et de récupérer au prochain démarrage. En raison de mon hostilité déraisonnable envers Python et de mon attitude très chaleureuse envers Qt, j'ai choisi Qt & C ++. Écrire des classes et des fonctions spaghetti pour analyser JSON est toujours un plaisir, j'avais besoin d'une solution universelle et en même temps facile à mon problème. "Nous devons comprendre", me suis-je dit.

Tout d'abord, un peu sur les termes:
La sérialisation est le processus de traduction d'une structure de données en une séquence de bits. L'inverse de l'opération de sérialisation est l'opération de désérialisation (structuration) - la restauration de l'état initial de la structure de données à partir d'une séquence de bits.
Go a un package d' encodage / json «natif» très utile qui vous permet de terminer la sérialisation de l'objet en utilisant la méthode Marshal et la structuration inverse en utilisant Unmarshal (à cause de cette bibliothèque, j'ai d'abord eu une idée incorrecte sur le marshaling, mais Desine sperare qui hic intras ) . En suivant les concepts de ce paquet, j'ai trouvé une autre bibliothèque pour Java - GSON , qui s'est avérée être un produit très agréable, c'était un plaisir de l'utiliser.

J'ai réfléchi à ce que j'aime de ces bibliothèques et suis arrivé à la conclusion que c'était leur facilité d'utilisation. Fonctionnalité flexible et tout en un appel, pour la sérialisation en JSON, il suffisait d'appeler la méthode toJson et de lui passer l'objet sérialisable. Cependant, C ++ lui-même n'a pas, par défaut, les capacités de métaobjet appropriées pour fournir suffisamment d'informations sur les champs d'une classe, comme cela se fait, par exemple, en Java (ClassName.class).

Je n'aimais QJson que pour la plate - forme Qt , mais cela ne correspondait pas tout à fait à ma compréhension de la facilité d'utilisation générée par les bibliothèques susmentionnées. Le projet est donc apparu, qui sera discuté ici.

Petit avertissement:de tels mécanismes ne résoudront pas pour vous le problème de l'interprétation des données. Tout ce que vous pouvez en tirer, c'est la conversion des données sous une forme plus pratique pour vous.

Structure du projet QSerializer


Le projet et les exemples peuvent être consultés sur GitHub ( lien vers le référentiel ). Des instructions d'installation détaillées y sont également fournies.

Anticipant le suicide architectural, je ferai une réserve que ce n'est pas la version finale. Les travaux se poursuivront malgré les pierres abandonnées, mais en tenant compte des souhaits.
Dépendances structurelles générales de la bibliothèque QSerializer

L'objectif principal de ce projet est de rendre la sérialisation à l'aide d'un format de données convivial en C ++ accessible et élémentaire. La clé du développement de la qualité et de la maintenance du produit est son architecture. Je n'exclus pas que d'autres modes de mise en œuvre puissent apparaître dans les commentaires de cet article, j'ai donc laissé un peu «d'espace pour la créativité». Si vous modifiez l'implémentation, vous pouvez soit ajouter une nouvelle implémentation de l'interface PropertyKeeper, soit modifier les méthodes d'usine afin de ne rien changer dans les fonctions QSerializer.

Déclaration de champ


Une façon de collecter des informations sur les méta-objets dans Qt est de les décrire dans le système de méta-objets de Qt lui-même. C'est peut-être le moyen le plus simple. MOC générera toutes les métadonnées nécessaires au moment de la compilation. Vous pouvez appeler la méthode metaObject sur l'objet décrit, qui renverra une instance de la classe QMetaObject, avec laquelle nous devons travailler.

Pour déclarer des champs à sérialiser, vous devez hériter la classe de QObject et y inclure la macro Q_OBJECT , afin d'indiquer clairement au MOC la qualification du type de classe en tant que type de base de QObject.

Ensuite, la macro Q_PROPERTY décrit les membres de la classe. Nous appellerons la propriété propriété décrite dans Q_PROPERTY . QSerializer ignorera la propriété sans que l'indicateur USER soit défini sur true.

Pourquoi le drapeau USER
, , QML. . , Q_PROPERTY QML QSerializer .

class User : public QObject
{
Q_OBJECT
// Define data members to be serialized
Q_PROPERTY(QString name MEMBER name USER true)
Q_PROPERTY(int age MEMBER age USER true)
Q_PROPERTY(QString email MEMBER email USER true)
Q_PROPERTY(std::vector<QString> phone MEMBER phone USER true)
Q_PROPERTY(bool vacation MEMBER vacation USER true)
public:
  // Make base constructor
  User() { }
 
  QString name;
  int age{0};
  QString email;
  bool vacation{false};
  std::vector<QString> phone; 
};

Pour déclarer des types d'utilisateurs non standard dans le système de méta-objets Qt, je suggère d'utiliser la macro QS_REGISTER , qui est définie dans qserializer.h. QS_REGISTER automatise le processus d'enregistrement des variations de type. Cependant, vous pouvez utiliser la méthode classique d'enregistrement de types avec qRegisterMetaType < T > (). Pour un système de méta-objet, le type de classe ( T ) et le pointeur de classe ( T *) sont des types complètement différents; ils auront des identifiants différents dans la liste des types généraux.

#define QS_METATYPE(Type) qRegisterMetaType<Type>(#Type) ;
#define QS_REGISTER(Type)       \
QS_METATYPE(Type)               \
QS_METATYPE(Type*)              \
QS_METATYPE(std::vector<Type*>) \
QS_METATYPE(std::vector<Type>)  \

class User;
void main()
{
// define user-type in Qt meta-object system
QS_REGISTER(User)
...
}

Espace de noms QSerializer


Comme vous pouvez le voir sur le diagramme UML, QSerializer contient un certain nombre de fonctions de sérialisation et de structuration. L'espace de noms reflète conceptuellement l'essence déclarative de QSerializer. La fonctionnalité intégrée est accessible via le nom de QSerializer, sans avoir besoin de créer un objet n'importe où dans le code.

En utilisant l'exemple de construction de JSON basé sur l'objet de la classe User décrit ci-dessus, il vous suffit d'appeler la méthode QSerializer :: toJson:

User u;
u.name = "Mike";
u.age = 25;
u.email = "example@exmail.com";
u.phone.push_back("+12345678989");
u.phone.push_back("+98765432121");
u.vacation = true;
QJsonObject json = QSerializer::toJson(&u);

Et voici le JSON résultant:

{
    "name": "Mike",
    "age": 25,
    "email": "example@exmail.com",
    "phone": [
        "+12345678989",
        "+98765432121"
    ],
    "vacation": true
}

Il existe deux façons de structurer un objet:

  • Si vous devez modifier un objet
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • Si vous avez besoin d'obtenir un nouvel objet
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

Plus d'exemples et de résultats peuvent être vus dans le dossier d' exemples .

Gardiens


Pour organiser l'écriture et la lecture pratiques des propriétés déclarées, QSerializer utilise des classes Keepers , chacune d'entre elles stockant un pointeur sur un objet (descendant de QObject) et l'un de ses QMetaProperty. QMetaProperty lui-même n'a pas de valeur particulière, en fait, c'est seulement un objet avec une description de la classe de propriétés qui a été déclarée pour le MOC. Pour lire et écrire, vous avez besoin d'un objet de classe spécifique, où cette propriété est décrite - c'est la principale chose dont vous devez vous souvenir.

Chaque champ sérialisable pendant la sérialisation est transmis au dépositaire du type correspondant. Des gardiens sont nécessaires pour encapsuler la fonctionnalité de sérialisation et de structuration pour une implémentation spécifique pour un type spécifique de données décrites. J'ai mis en évidence 4 types:

  • QMetaSimpleKeeper - gardien de propriété avec des types de données primitifs
  • QMetaArrayKeeper - gardien de propriété avec des tableaux de données primitives
  • QMetaObjectKeeper - gardien des objets imbriqués
  • QMetaObjectArrayKeeper - gardien des tableaux d'objets imbriqués

Flux de données

Au cœur des dépositaires de données primitifs se trouve la conversion des informations de JSON / XML vers QVariant et vice versa, car QMetaProperty fonctionne avec QVariant par défaut.

QMetaProperty prop;
QObject * linkedObj;
...
std::pair<QString, QJsonValue> QMetaSimpleKeeper::toJson()
{
    QJsonValue result = QJsonValue::fromVariant(prop.read(linkedObj));
    return std::make_pair(QString(prop.name()), result);
}

void QMetaSimpleKeeper::fromJson(const QJsonValue &val)
{
    prop.write(linkedObj, QVariant(val));
}

Les conservateurs d'objets sont basés sur le transfert d'informations de JSON / XML vers une série d'autres conservateurs et vice versa. Ces dépositaires travaillent avec leur propriété en tant qu'objet distinct, qui peut également avoir ses propres dépositaires, leur tâche consiste à collecter des données sérialisées à partir de l'objet propriété et à structurer l'objet propriété en fonction des données disponibles.

QMetaProperty prop;
QObject * linkedObj;
...
void QMetaObjectKeeper::fromJson(const QJsonValue &json)
{
    ...
    QSerializer::fromJson(linkedObj, json.toObject());
}

std::pair<QString, QJsonValue> QMetaObjectKeeper::toJson()
{
    QJsonObject result = QSerializer::toJson(linkedObj);;
    return std::make_pair(prop.name(),QJsonValue(result));
}

Les gardiens implémentent l'interface PropertyKeeper, dont la classe abstraite de base des gardiens est héritée. Cela vous permet d'analyser et de composer des documents au format XML ou JSON séquentiellement de haut en bas, en descendant simplement les propriétés stockées décrites et en approfondissant lorsque vous descendez dans les objets incorporés, le cas échéant, dans les propriétés décrites, sans entrer dans les détails de l'implémentation.

Interface PropertyKeeper
class PropertyKeeper
{
public:
    virtual ~PropertyKeeper() = default;
    virtual std::pair<QString, QJsonValue> toJson() = 0;
    virtual void fromJson(const QJsonValue&) = 0;
    virtual std::pair<QString, QDomNode> toXml() = 0;
    virtual void fromXml(const QDomNode &) = 0;
};

Guardian Factory


Étant donné que tous les dépositaires implémentent une interface, toutes les implémentations sont masquées derrière un écran pratique, et un ensemble de ces implémentations est fourni par la fabrique KeepersFactory. À partir de l'objet transféré à l'usine, vous pouvez obtenir une liste de toutes les propriétés déclarées via son QMetaObject, en fonction du type de dépositaire déterminé.

Mise en œuvre de KeepersFactory Factory
    const std::vector<int> simple_t =
    {
        qMetaTypeId<int>(),
        qMetaTypeId<bool>(),
        qMetaTypeId<double>(),
        qMetaTypeId<QString>(),
    };

    const std::vector<int> array_of_simple_t =
    {
        qMetaTypeId<std::vector<int>>(),
        qMetaTypeId<std::vector<bool>>(),
        qMetaTypeId<std::vector<double>>(),
        qMetaTypeId<std::vector<QString>>(),
    };
...
PropertyKeeper *KeepersFactory::getMetaKeeper(QObject *obj, QMetaProperty prop)
{
    int t_id = QMetaType::type(prop.typeName());
    if(std::find(simple_t.begin(), simple_t.end(), t_id) != simple_t.end())
        return new QMetaSimpleKeeper(obj,prop);
    else if (std::find(array_of_simple_t.begin(),array_of_simple_t.end(), t_id) != array_of_simple_t.end())
    {
        if( t_id == qMetaTypeId<std::vector<int>>())
            return new QMetaArrayKeeper<int>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<QString>>())
            return new QMetaArrayKeeper<QString>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<double>>())
            return new QMetaArrayKeeper<double>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<bool>>())
            return new QMetaArrayKeeper<bool>(obj, prop);
    }
    else
    {
        QObject * castobj = qvariant_cast<QObject *>(prop.read(obj));
        if(castobj)
            return new QMetaObjectKeeper(castobj,prop);
        else if (QString(prop.typeName()).contains("std::vector<"))
        {
            QString t = QString(prop.typeName()).remove("std::vector<").remove(">");
            int idOfElement = QMetaType::type(t.toStdString().c_str());
            if(QMetaType::typeFlags(idOfElement).testFlag(QMetaType::PointerToQObject))
                return new QMetaObjectArrayKeeper(obj, prop);
        }
    }
    throw QSException(UnsupportedPropertyType);
}

std::vector<PropertyKeeper *> KeepersFactory::getMetaKeepers(QObject *obj)
{
    std::vector<PropertyKeeper*> keepers;
    for(int i = 0; i < obj->metaObject()->propertyCount(); i++)
    {
        if(obj->metaObject()->property(i).isUser(obj))
            keepers.push_back(getMetaKeeper(obj, obj->metaObject()->property(i)));
    }
    return keepers;
}
...


Une caractéristique clé de la fabrique de gardiens est la possibilité de fournir une série complète de gardiens pour un objet, et vous pouvez étendre la liste des types primitifs pris en charge en modifiant des collections constantes avec des identificateurs de type. Chaque série de gardiens est une sorte de carte pour les propriétaires de l'objet. Lorsqu'un objet KeepersFactory est détruit, la mémoire allouée à la série de keepers qu'il fournit est libérée.

Limitations et comportement

SituationComportement
Tenter de sérialiser un objet dont le type n'est pas hérité de QObjectErreur de compilation
Type non déclaré lors d'une tentative de sérialisation / struturisationException QSException :: UnsupportedPropertyType
Une tentative de sérialiser / structurer un objet avec un type primitif différent de celui décrit dans les collections simple_t et array_of_simple_t.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


À mon avis, le projet s'est avéré utile, car cet article a été écrit. Pour ma part, j'ai conclu qu'il n'y a pas de solutions universelles, il faut toujours sacrifier quelque chose. En développant une flexibilité, en termes d'utilisation, de fonctionnalité, vous tuez la simplicité, et vice versa.

Je ne vous conseille pas d'utiliser QSerializer, mon objectif est plutôt mon propre développement en tant que programmeur. Bien sûr, je poursuis également l'objectif d'aider quelqu'un, mais en premier lieu - simplement me faire plaisir. Sois positif)

All Articles