QSerializer: solución para la serialización JSON / XML simple

Hola Habr!

Pensé que de alguna manera resulta injusto: en Java, C #, Go, Python, etc. Hay bibliotecas para la serialización cómoda de datos de objetos en JSON y XML ya de moda, pero en C ++ lo olvidaron o no quisieron, o realmente no lo necesitaban, o todo es complicado, o tal vez todo junto. Entonces decidí arreglar esto.

Todos los detalles, como siempre, debajo del corte.

imagen

Antecedentes


Una vez más, decidí retomar el próximo proyecto favorito, cuya esencia era el intercambio cliente-servidor, mientras que el servidor favorito de muchos era RaspberryPi. Entre otras cosas, estaba interesado en la cuestión de crear "puntos de guardado", de modo que pudiera, lo más simple posible, dentro del marco del prototipo, guardar el estado del objeto antes de salir y recuperarme en el próximo inicio. Debido a mi hostilidad irracional hacia Python y mi actitud muy cálida hacia Qt, elegí Qt & C ++. Escribir clases y funciones de espagueti para analizar JSON seguía siendo un placer, necesitaba una solución universal y al mismo tiempo fácil para mi problema. "Tenemos que resolverlo", me dije.

Primero, un poco sobre los términos:
La serialización es el proceso de traducir una estructura de datos en una secuencia de bits. La inversa de la operación de serialización es la operación de deserialización (estructuración): la restauración del estado inicial de la estructura de datos a partir de una secuencia de bits.
Go tiene un paquete de codificación / json "nativo" muy útil que le permite completar la serialización del objeto utilizando el método Marshal y la estructura inversa utilizando Unmarshal (debido a esta biblioteca, primero tuve una idea incorrecta sobre la clasificación, pero Desine sperare qui hic intras ) . Siguiendo los conceptos de este paquete, encontré otra biblioteca para Java: GSON , que resultó ser un producto muy agradable, fue un placer usarlo.

Pensé en lo que me gusta de estas bibliotecas y llegué a la conclusión de que era su facilidad de uso. Funcionalidad flexible y llamada todo en uno, para la serialización en JSON fue suficiente llamar al método toJson y pasarle el objeto serializable. Sin embargo, por sí mismo, C ++ no tiene capacidades de metaobjetos adecuadas de forma predeterminada para proporcionar suficiente información sobre los campos de una clase, como se hace, por ejemplo, en Java (ClassName.class).

Solo me gustó QJson para la plataforma Qt , pero aún así no encajaba en mi comprensión de la facilidad de uso generada por las bibliotecas anteriores. Entonces apareció el proyecto, que se discutirá aquí.

Pequeño descargo de responsabilidad:dichos mecanismos no resolverán el problema de interpretación de datos para usted. Todo lo que puede obtener de ellos es la conversión de datos en una forma más conveniente para usted.

Estructura del proyecto QSerializer


El proyecto y los ejemplos se pueden ver en GitHub ( enlace al repositorio ). Las instrucciones detalladas de instalación también se dan allí.

Anticipando el suicidio arquitectónico, haré una reserva de que esta no es la versión final. El trabajo continuará a pesar de las piedras abandonadas, pero teniendo en cuenta los deseos.
Dependencias estructurales generales de la biblioteca QSerializer

El objetivo principal de este proyecto es hacer que la serialización utilizando un formato de datos amigable para el usuario en C ++ sea accesible y elemental. La clave para el desarrollo y mantenimiento de calidad del producto es su arquitectura. No excluyo que otras formas de implementación puedan aparecer en los comentarios a este artículo, así que dejé un pequeño "espacio para la creatividad". Si cambia la implementación, puede agregar una nueva implementación de la interfaz PropertyKeeper o cambiar los métodos de fábrica para que no tenga que cambiar nada en las funciones de QSerializer.

Declaración de campo


Una forma de recopilar información de metaobjetos en Qt es describirla en el sistema de metaobjetos de Qt. Quizás esta sea la forma más fácil. MOC generará todos los metadatos necesarios en tiempo de compilación. Puede llamar al método metaObject en el objeto descrito, que devolverá una instancia de la clase QMetaObject, con la que tenemos que trabajar.

Para declarar que los campos se serializarán, debe heredar la clase de QObject e incluir la macro Q_OBJECT en ella , para dejar en claro al MOC sobre la calificación del tipo de clase como base de QObject.

A continuación, la macro Q_PROPERTY describe los miembros de la clase. Llamaremos a la propiedad propiedad descrita en Q_PROPERTY . QSerializer ignorará la propiedad sin el indicador USER establecido en verdadero.

Por qué bandera USUARIO
, , 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; 
};

Para declarar tipos de usuario no estándar en el sistema de metaobjetos Qt, sugiero usar la macro QS_REGISTER , que se define en qserializer.h. QS_REGISTER automatiza el proceso de registrar variaciones de tipo. Sin embargo, puede usar el método clásico de registrar tipos con qRegisterMetaType < T > (). Para un sistema de metaobjetos, el tipo de clase ( T ) y el puntero de clase ( T *) son tipos completamente diferentes; tendrán identificadores diferentes en la lista general de tipos.

#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)
...
}

Espacio de nombres QSerializer


Como puede ver en el diagrama UML, QSerializer contiene una serie de funciones para la serialización y estructuración. El espacio de nombres refleja conceptualmente la esencia declarativa de QSerializer. Se puede acceder a la funcionalidad integrada a través del nombre de QSerializer, sin la necesidad de crear un objeto en cualquier parte del código.

Usando el ejemplo de compilación de JSON basado en el objeto de la clase Usuario descrita anteriormente, solo necesita llamar al método 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);

Y aquí está el JSON resultante:

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

Hay dos formas de estructurar un objeto:

  • Si necesita modificar un objeto
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • Si necesita obtener un nuevo objeto
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

Se pueden ver más ejemplos y resultados en la carpeta de ejemplos .

Cuidadores


Para organizar la escritura y lectura conveniente de las propiedades declaradas, QSerializer utiliza las clases Keepers , cada una de las cuales almacena un puntero a un objeto (descendiente QObject) y uno de sus QMetaProperty. QMetaProperty en sí no tiene un valor particular, de hecho, es solo un objeto con una descripción de la clase de propiedad que se declaró para el MOC. Para leer y escribir, necesita un objeto específico de la clase donde se describe esta propiedad; esto es lo más importante para recordar.

Cada campo serializable durante la serialización se pasa al custodio del tipo correspondiente. Se necesitan encargados para encapsular la funcionalidad de serialización y estructuración para una implementación específica para un tipo específico de datos descritos. Destaqué 4 tipos:

  • QMetaSimpleKeeper - guardián de la propiedad con tipos de datos primitivos
  • QMetaArrayKeeper - guardián de la propiedad con matrices de datos primitivos
  • QMetaObjectKeeper - guardián de objetos anidados
  • QMetaObjectArrayKeeper - guardián de matrices de objetos anidados

Flujo de datos

Los conservadores de datos primitivos se basan en la conversión de información de JSON / XML a QVariant y viceversa, porque QMetaProperty funciona con QVariant de forma predeterminada.

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));
}

Los guardianes de objetos se basan en la transferencia de información de JSON / XML a una serie de otros guardianes y viceversa. Dichos custodios trabajan con su propiedad como un objeto separado, que también puede tener sus propios custodios, su tarea es recopilar datos serializados del objeto de propiedad y estructurar el objeto de propiedad de acuerdo con los datos 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));
}

Los encargados implementan la interfaz PropertyKeeper, de la cual se hereda la clase abstracta base de cuidadores. Esto le permite analizar y componer documentos en formato XML o JSON secuencialmente de arriba a abajo, simplemente bajando las propiedades almacenadas descritas y profundizando a medida que desciende a los objetos incrustados, si los hay, en las propiedades descritas, sin entrar en detalles de la implementación.

Interfaz 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


Como todos los custodios implementan una interfaz, todas las implementaciones están ocultas detrás de una pantalla conveniente, y la fábrica KeepersFactory proporciona un conjunto de estas implementaciones. Desde el objeto transferido a la fábrica, puede obtener una lista de todas las propiedades declaradas a través de su QMetaObject, en función del cual se determina el tipo de custodia.

Implementación de fábrica de KeepersFactory
    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;
}
...


Una característica clave de la fábrica de guardianes es la capacidad de proporcionar una serie completa de guardianes para un objeto, y puede expandir la lista de tipos primitivos compatibles editando colecciones constantes con identificadores de tipo. Cada serie de guardianes es una especie de mapa de propiedades para el objeto. Cuando se destruye un objeto KeepersFactory, se libera la memoria asignada para la serie de guardianes que proporciona.

Limitaciones y comportamiento

SituaciónComportamiento
Intente serializar un objeto cuyo tipo no se hereda de QObjectError de compilación
Tipo no declarado al intentar la serialización / struturizaciónExcepción QSException :: UnsupportedPropertyType
Un intento de serializar / estructurar un objeto con un tipo primitivo diferente al descrito en las colecciones simple_t y array_of_simple_t.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


En mi opinión, el proyecto resultó valioso, porque este artículo fue escrito. Por mi parte, llegué a la conclusión de que no hay soluciones universales, siempre hay que sacrificar algo. Al desarrollar una funcionalidad flexible, en términos de uso, elimina la simplicidad y viceversa.

No le recomiendo que use QSerializer, mi objetivo es más bien mi propio desarrollo como programador. Por supuesto, también persigo el objetivo de ayudar a alguien, pero en primer lugar, solo obtener placer. Ser positivo)

All Articles