QSerializer está muerto, viva QSerializer

Han pasado varios meses desde que hablé aquí sobre mi proyecto de biblioteca basada en Qt para serializar datos desde una vista de objeto a JSON / XML y viceversa.



Y no importa lo orgulloso que esté de la arquitectura construida, debo admitir que la implementación resultó, francamente, controvertida.



Todo esto dio como resultado un procesamiento a gran escala, cuyos resultados se discutirán en este artículo. Para obtener más detalles, ¡debajo del corte!







QSerializer murió



QSerializer tenía inconvenientes, cuya solución a menudo se convirtió en un inconveniente aún mayor, aquí hay algunos de ellos:



  • Muy costoso (serialización, mantener a los propietarios en el montón, controlar la vida útil de los propietarios, etc.)
  • Trabajando solo con clases basadas en QObject
  • Los objetos "complejos" anidados y sus colecciones también deben estar basados ​​en QObject
  • Incapacidad para complementar colecciones durante la deserialización
  • Solo anidamiento teóricamente infinito
  • La imposibilidad de trabajar con tipos importantes de objetos "complejos", debido a la prohibición de copiar desde QObject
  • La necesidad de un registro obligatorio de tipos en el sistema de metaobjetos Qt
  • Problemas comunes de "biblioteca", como problemas de vinculación y portabilidad entre plataformas


Entre otras cosas, quería ser capaz de serializar cualquier objeto "aquí y ahora", cuando esto tenía que usar un gran enlace de métodos en el espacio de nombres de QSerializer.



¡Viva QSerializer!



QSerializer no estaba completo. Era necesario idear una solución en la que el usuario no dependiera del QObject, fuera posible trabajar con tipos de valor y a un menor costo.



En un comentario al artículo anterior , el usuariomicrolanotó que puede pensar en usar Q_GADGET .



Ventajas Q_GADGET :



  • No hay restricciones para copiar
  • Tiene una instancia estática de QMetaObject para acceder a las propiedades


Dependiendo de Q_GADGET , tuve que reconsiderar el enfoque sobre cómo crear JSON y XML en función de los campos de clase declarados. El problema del "alto costo" se manifestó principalmente debido a:



  • Gran tamaño de clase de almacenamiento (al menos 40 bytes)
  • Asignar un montón de nuevas entidades guardianas para cada propiedad y controlar su TTL


Para reducir el costo, formulé el siguiente requisito:

La presencia en cada objeto serializable de métodos de cable para serialización / deserialización de todas las propiedades de la clase y la presencia de métodos para leer y escribir valores para cada propiedad usando el formato asignado para esta propiedad.

Macros



Evitar la tipificación fuerte de C ++ que complica la serialización automática no es fácil, y la experiencia previa lo ha demostrado. Las macros, por otro lado, pueden servir como una excelente ayuda para resolver tal problema (casi todo el sistema de meta-objetos Qt está construido sobre macros), porque usando macros, puede generar código de métodos y propiedades.



Sí, las macros suelen ser malas en su forma más pura: son casi imposibles de depurar. Podría comparar escribir una macro para generar código con poner un zapato de cristal en el talón de su jefe, ¡pero difícil no significa imposible!



Digresión lírica sobre macros

— , , «» (). .



QSerializer actualmente proporciona 2 formas de declarar una clase como serializable: heredar de la clase QSerializer o usar la macro de generación de código QS_CLASS .



En primer lugar, debe definir la macro Q_GADGET en el cuerpo de la clase, esto da acceso al staticMetaObject, almacenará las propiedades generadas por las macros.



Heredar de QSerializer le permitirá convertir múltiples objetos serializables a un tipo y serializarlos a granel.



La clase QSerializer contiene 4 métodos de explorador que le permiten analizar las propiedades de un objeto y un método virtual para obtener una instancia de un QMetaObject:



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


Q_GADGET no tiene todos los enlaces de metaobjetos que proporciona Q_OBJECT .



Dentro del QSerializer, la instancia staticMetaObject representará la clase QSerializer, pero no derivará de ella de ninguna manera, por lo que al crear la clase basada en QSerializer, debe anular el método metaObject. Puede agregar la macro QS_SERIALIZER al cuerpo de la clase y anulará el método metaObject por usted.



Además, usar staticMetaObject en lugar de almacenar una instancia de QMetaObject en cada objeto ahorra 40 bytes del tamaño de la clase, bueno, en general, ¡belleza!



Si no desea heredar por alguna razón, puede definir la macro QS_CLASS en el cuerpo de la clase serializada, generará todos los métodos requeridos en lugar de heredar de QSerializer.



Declaración de campos



Por separado, hay 4 tipos de datos serializables en JSON y XML, sin los cuales la serialización a estos formatos no será completa. La tabla muestra los tipos de datos y las macros correspondientes como forma de describir:

Tipo de datos Descripción Macro
campo campo ordinario de tipo primitivo (varios números, cadenas, banderas) QS_FIELD
colección conjunto de valores de tipos de datos primitivos QS_COLLECTION
un objeto estructura compleja de campos u otras estructuras complejas QS_OBJECT
colección de objetos un conjunto de estructuras de datos complejas del mismo tipo QS_COLLECTION_OBJECTS


Supondremos que el código que genera estas macros se llama descripción y las macros que las generan se denominan descriptivas.



Solo hay un principio para generar una descripción: para un campo específico, generar propiedades JSON y XML y definir métodos para escribir / leer valores.



Analicemos la generación de una descripción JSON usando el ejemplo de un campo de tipo de datos primitivo:



/* 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)  


Para el campo de dígitos int, se generará un dígito de propiedad con el tipo QJsonValue y se definirán métodos privados de escritura y lectura: get_json_digit y set_json_digit, que luego se convertirán en conductores para serializar / deserializar el campo de dígitos usando JSON.



¿Como sucedió esto?
name digit, ('##') digit — .



type int. , type int . QVariant int .



Y aquí está la generación de una descripción JSON para una estructura compleja:



/* 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)


Los objetos complejos son un conjunto de propiedades anidadas que funcionarán como una propiedad "grande" para una clase externa, porque dichos objetos también tendrán métodos de conexión. Todo lo que necesita hacer para esto es llamar al método de guía apropiado en los métodos de lectura y escritura de estructuras complejas.



Creación de clases



Por lo tanto, tenemos una infraestructura bastante simple para crear una clase serializable.



Entonces, por ejemplo, puede hacer que una clase sea serializable heredando de QSerializer:



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


O así, usando la macro QS_CLASS :



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


Ejemplo de serialización 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.



Limitaciones



Campos únicos Los



tipos primitivos o definidos por el usuario deben proporcionar un constructor predeterminado.



Colecciones



La clase de colección debe tener una plantilla y proporcionar métodos claros, en tamaño y anexos. Puede utilizar sus propias colecciones, sujeto a las condiciones. Colecciones de Qt que cumplen estas condiciones: QVector, QStack, QList, QQueue.



Versiones Qt



Versión mínima Qt 5.5.0 Versión

mínima probada Qt 5.9.0 Versión

máxima probada Qt 5.15.0

NOTA: puede participar en pruebas y probar QSerializer en versiones anteriores de Qt



Salir



Al reelaborar QSerializer, absolutamente no me propuse la tarea de reducirlo significativamente. Sin embargo, su tamaño se redujo de 9 archivos a 1, lo que también redujo su complejidad. Ahora QSerializer ya no es una biblioteca en nuestra forma habitual, ahora es solo un archivo de encabezado, que es suficiente para incluirlo en el proyecto y obtener toda la funcionalidad para una serialización / deserialización cómoda. El desarrollo comenzó en marzo, se inventó una arquitectura complicada y el proyecto se llenó de dependencias, muletas, reescrito desde 0 varias veces. Y todo para que finalmente se convierta en un pequeño archivo.



Preguntándome: "¿Valió la pena el esfuerzo invertido en ello?", Respondo: "Sí, lo fue". Ya lo probé en mis proyectos de combate y el resultado me gustó.



Enlaces

GitHub: enlace

Última versión: v1.1

Artículo anterior: QSerializer: solución para serialización JSON / XML simple



Lista futura



  • Reducción sustancial de costos (se puede hacer incluso más barato)
  • Compacidad
  • Trabajar con tipos importantes
  • Descripción básica de datos serializables
  • Compatibilidad con cualquier colección con plantilla que proporcione métodos claros, at, dimensionar y adjuntar. Incluso los suyos
  • Colecciones completamente mutables sobre deserialización
  • Soporte para todos los tipos primitivos populares
  • Soporte para cualquier tipo personalizado descrito usando QSerializer
  • No es necesario registrar tipos personalizados



All Articles