QSerializer est mort, vive QSerializer

Plusieurs mois se sont écoulés depuis lors, car j'ai décrit ici son projet de bibliothèque basée sur Qt pour sérialiser les données du type d'objet en JSON / XML et inversement.



Et aussi fier que je sois de l'architecture construite, je dois l'admettre - la mise en œuvre s'est avérée, franchement, controversée.



Tout cela a abouti à une révision à grande échelle, dont les résultats seront discutés dans cet article. Pour plus de détails - sous la coupe!







QSerializer est mort



QSerializer avait des inconvénients, dont la solution devenait souvent un inconvénient encore plus important, en voici quelques-uns:



  • Très coûteux (sérialisation, garder les propriétaires sur le tas, contrôler la durée de vie des détenteurs, etc.)
  • Travailler uniquement avec les classes basées sur QObject
  • Les objets "complexes" imbriqués et leurs collections doivent également être basés sur QObject
  • Impossibilité de compléter les collections lors de la désérialisation
  • Seulement une imbrication théoriquement infinie
  • L'incapacité de travailler avec des types importants d'objets "complexes", en raison de l'interdiction de copier à partir de QObject
  • La nécessité d'un enregistrement obligatoire des types dans le système de méta-objets Qt
  • Problèmes de «bibliothèque» courants tels que les problèmes de liaison et de portabilité entre les plates-formes


Entre autres choses, je voulais être capable de sérialiser n'importe quel objet "ici et maintenant", alors que cela devait utiliser une énorme liaison de méthodes dans l'espace de noms QSerializer.



Vive QSerializer!



QSerializer n'était pas terminé. Il était nécessaire de trouver une solution dans laquelle l'utilisateur ne dépendrait pas du QObject, il serait possible de travailler avec des types valeur et à moindre coût.



Dans un commentaire à l' article précédent , l'utilisateurmicrolaremarqué que vous pouvez penser à utiliser Q_GADGET .



Avantages Q_GADGET :



  • Aucune restriction sur la copie
  • Possède une instance statique de QMetaObject pour accéder aux propriétés


En m'appuyant sur Q_GADGET , j'ai dû reconsidérer l'approche de la création de JSON et XML en fonction des champs de classe déclarés. Le problème du "coût élevé" s'est manifesté principalement en raison:



  • Grande taille de classe de stockage (au moins 40 octets)
  • Allocation d'un tas pour de nouvelles entités gardiennes pour chaque propriété et contrôle de leur TTL


Pour réduire le coût, j'ai formulé l'exigence suivante:

La présence dans chaque objet sérialisable de méthodes filaires pour sérialiser / désérialiser toutes les propriétés de la classe et la présence de méthodes de lecture et d'écriture des valeurs pour chaque propriété en utilisant le format attribué à cette propriété

Macros



Contourner le typage fort de C ++ qui complique la sérialisation automatique n'est pas facile, et l'expérience précédente l'a montré. Les macros, par contre, peuvent être une excellente aide pour résoudre un tel problème (presque tout le système de méta-objets Qt est construit sur des macros), car en utilisant des macros, vous pouvez générer du code de méthodes et de propriétés.



Oui, les macros sont souvent mauvaises dans leur forme la plus pure - elles sont presque impossibles à déboguer. Je pourrais comparer l'écriture d'une macro pour générer du code à mettre une chaussure de cristal sur le talon de votre patron, mais difficile ne veut pas dire impossible!



Digression lyrique sur les macros

— , , «» (). .



QSerializer propose actuellement 2 façons de déclarer une classe comme sérialisable: hériter de la classe QSerializer ou utiliser la macro de génération de code QS_CLASS .



Tout d'abord, vous devez définir la macro Q_GADGET dans le corps de la classe, cela donne accès au staticMetaObject, il stockera les propriétés générées par les macros.



L'héritage de QSerializer vous permettra de convertir plusieurs objets sérialisables en un seul type et de les sérialiser en masse.



La classe QSerializer contient 4 méthodes d'exploration qui vous permettent d'analyser les propriétés d'un objet et une méthode virtuelle pour obtenir une instance d'un QMetaObject:



QJsonValue toJson() const
void fromJson(const QJsonValue &)
QDomNode toXml() const
void fromXml(const QDomNode &)
virtual const QMetaObject * metaObject() const


Q_GADGET n'a pas toutes les liaisons de méta-objets fournies par Q_OBJECT .



À l'intérieur du QSerializer, l'instance staticMetaObject représentera la classe QSerializer, mais n'en dérivera d'aucune façon, donc lors de la création de la classe basée sur QSerializer, vous devez remplacer la méthode metaObject. Vous pouvez ajouter la macro QS_SERIALIZER au corps de la classe et elle remplacera la méthode metaObject pour vous.



De plus, utiliser staticMetaObject au lieu de stocker une instance de QMetaObject dans chaque objet économise 40 octets de la taille de la classe, enfin, en général, beauté!



Si vous ne souhaitez pas hériter pour une raison quelconque, vous pouvez définir la macro QS_CLASS dans le corps de la classe sérialisée, il générera toutes les méthodes requises au lieu d'hériter de QSerializer.



Déclaration des champs



Séparément, il existe 4 types de données sérialisables en JSON et XML, sans lesquelles la sérialisation vers ces formats ne sera pas complète. Le tableau présente les types de données et les macros correspondantes pour décrire:

Type de données La description Macro
champ champ ordinaire de type primitif (divers nombres, chaînes, drapeaux) QS_FIELD
collection ensemble de valeurs de types de données primitifs QS_COLLECTION
un objet structure complexe de champs ou autres structures complexes QS_OBJECT
collection d'objets un ensemble de structures de données complexes du même type QS_COLLECTION_OBJECTS


Nous supposerons que le code qui génère ces macros est appelé une description, et les macros qui le génèrent sont appelées descriptives.



Il n'y a qu'un seul principe pour générer une description - pour un champ spécifique, générer des propriétés JSON et XML et définir des méthodes pour écrire / lire des valeurs.



Analysons la génération d'une description JSON à l'aide de l'exemple d'un champ de type de données primitif:



/* Create JSON property and methods for primitive type field*/
#define QS_JSON_FIELD(type, name)                                                           
    Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)                  
    private:                                                                                
        QJsonValue get_json_##name() const {                                                
            QJsonValue val = QJsonValue::fromVariant(QVariant(name));                       
            return val;                                                                     
        }                                                                                   
        void set_json_##name(const QJsonValue & varname){                                   
            name = varname.toVariant().value<type>();                                       
        }   
...
int digit;
QS_JSON_FIELD(int, digit)  


Pour le champ int digit, un chiffre de propriété avec le type QJsonValue sera généré et des méthodes d'écriture et de lecture privées - get_json_digit et set_json_digit seront définis, ils deviendront alors des conducteurs pour la sérialisation / désérialisation du champ numérique à l'aide de JSON.



Comment cela peut-il arriver?
name digit, ('##') digit — .



type int. , type int . QVariant int .



Et voici la génération d'une description JSON pour une structure complexe:



/* Generate JSON-property and methods for some custom class */
/* Custom type must be provide methods fromJson and toJson */
#define QS_JSON_OBJECT(type, name)
    Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)
    private:
    QJsonValue get_json_##name() const {
        QJsonObject val = name.toJson();
        return QJsonValue(val);
    }
    void set_json_##name(const QJsonValue & varname) {
        if(!varname.isObject())
        return;
        name.fromJson(varname);
    } 
...
SomeClass object;
QS_JSON_OBJECT(SomeClass, object)


Les objets complexes sont un ensemble de propriétés imbriquées, qui pour la classe externe fonctionnera comme une «grande» propriété, car ces objets auront également des méthodes de fil. Pour cela, il vous suffit d'appeler la méthode de guidage appropriée dans les méthodes de lecture et d'écriture de structures complexes.



Création de classe



Ainsi, nous avons une infrastructure assez simple pour créer une classe sérialisable.



Ainsi, par exemple, vous pouvez rendre une classe sérialisable en héritant de QSerializer:



class SerializableClass : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};


Ou comme ceci, en utilisant la macro QS_CLASS :



class SerializableClass {
Q_GADGET
QS_CLASS
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
};


Exemple de sérialisation JSON
:



class CustomType : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, someInteger)
QS_FIELD(QString, someString)
};

class SerializableClass : public QSerializer {
Q_GADGET
QS_SERIALIZER
QS_FIELD(int, digit)
QS_COLLECTION(QList, QString, strings)
QS_OBJECT(CustomType, someObject)
QS_COLLECTION_OBJECTS(QVector, CustomType, objects)
};


, :



SerializableClass serializable;
serializable.someObject.someString = "ObjectString";
serializable.someObject.someInteger = 99999;
for(int i = 0; i < 3; i++) {
    serializable.digit = i;
    serializable.strings.append(QString("list of strings with index %1").arg(i));
    serializable.objects.append(serializable.someObject);
}
QJsonObject json = serializable.toJson();


JSON:



{
    "digit": 2,
    "objects": [
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        },
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        },
        {
            "someInteger": 99999,
            "someString": "ObjectString"
        }
    ],
    "someObject": {
        "someInteger": 99999,
        "someString": "ObjectString"
    },
    "strings": [
        "list of strings with index 0",
        "list of strings with index 1",
        "list of strings with index 2"
    ]
}


— , XML , toJson toXml.



example.



Limites



Champs uniques



Les types définis par l'utilisateur ou primitifs doivent fournir un constructeur par défaut.



Collections



La classe de collection doit être basée sur un modèle et fournir des méthodes clear, at, size et append. Vous pouvez utiliser vos propres collections, sous réserve des conditions. Collections Qt qui remplissent ces conditions: QVector, QStack, QList, QQueue.



Versions Qt



Version minimale Qt 5.5.0 Version

minimale testée Qt ​​5.9.0 Version

maximale testée Qt ​​5.15.0

REMARQUE: vous pouvez participer au test et au test de QSerializer sur les versions antérieures de Qt



Résultat



Lors de la retouche de QSerializer, je ne me suis absolument pas donné pour tâche de le réduire de manière significative. Cependant, sa taille est passée de 9 fichiers à 1, ce qui a également réduit sa complexité. Maintenant, QSerializer n'est plus une bibliothèque dans notre forme habituelle, maintenant c'est juste un fichier d'en-tête, ce qui est suffisant pour être inclus dans le projet et obtenir toutes les fonctionnalités pour une sérialisation / désérialisation confortable. Le développement a commencé en mars, une architecture délicate a été inventée et le projet a été envahi par des dépendances, des béquilles, réécrit plusieurs fois à partir de 0. Et tout cela pour finalement se transformer en un petit fichier.



En me demandant: «Cela valait-il l'effort consacré?», Je réponds: «Oui, ça l'était». Je l'ai déjà essayé sur mes projets de combat et le résultat m'a plu.



Liens

GitHub: lien

Dernière version: v1.1

Article précédent: QSerializer: solution pour la sérialisation JSON / XML simple



Liste future



  • Réduction substantielle des coûts (peut être fait encore moins cher)
  • Compacité
  • Travailler avec des types significatifs
  • Description de base des données sérialisables
  • Prise en charge de toute collection basée sur un modèle qui fournit des méthodes claires, at, size et append. Même le leur
  • Collections entièrement modifiables lors de la désérialisation
  • Prise en charge de tous les types primitifs populaires
  • Prise en charge de tout type personnalisé décrit à l'aide de QSerializer
  • Pas besoin d'enregistrer des types personnalisés



All Articles