QSerializer: solução para serialização JSON / XML simples

Olá Habr!

Eu pensei que de alguma forma isso acaba sendo injusto - em Java, C #, Go, Python, etc. Existem bibliotecas para serialização confortável de dados de objetos no atual JSON e XML, mas em C ++ eles esqueceram, ou não queriam ou realmente não precisavam, ou se tudo isso é complicado ou talvez todos juntos. Então eu decidi consertar isso.

Todos os detalhes, como de costume, sob o corte.

imagem

fundo


Mais uma vez, decidi assumir o próximo projeto de estimação, cuja essência era a troca cliente-servidor, enquanto o servidor favorito de muitos era o RaspberryPi. Entre outras coisas, eu estava interessado na questão de criar "pontos de salvamento" - para que pudesse, da maneira mais simples possível, dentro da estrutura do protótipo, salvar o estado do objeto antes de sair e me recuperar na próxima partida. Devido à minha hostilidade irracional ao Python e minha atitude calorosa em relação ao Qt, escolhi o Qt & C ++. Escrever aulas e funções de espaguete para analisar JSON ainda era um prazer, eu precisava de uma solução universal e ao mesmo tempo fácil para o meu problema. "Temos que descobrir", eu disse a mim mesma.

Primeiro, um pouco sobre os termos:
Serialização é o processo de converter uma estrutura de dados em uma sequência de bits. O inverso da operação de serialização é a operação de desserialização (estruturação) - a restauração do estado inicial da estrutura de dados a partir de uma sequência de bits.
O Go possui um pacote de codificação / json "nativo" muito útil que permite concluir a serialização do objeto usando o método Marshal e a estrutura reversa usando o Unmarshal (por causa desta biblioteca, eu tive uma idéia incorreta sobre empacotamento, mas Desine espera que intras ) . Seguindo os conceitos deste pacote, encontrei outra biblioteca para Java - GSON , que acabou sendo um produto muito agradável, foi um prazer usá-lo.

Pensei no que eu mais gostava nessas bibliotecas e cheguei à conclusão de que era a facilidade de uso delas. Funcionalidade flexível e tudo em uma chamada, para serialização em JSON, bastava chamar o método toJson e passar um objeto serializável para ele. No entanto, o próprio C ++ não possui, por padrão, recursos adequados de metaobjeto para fornecer informações suficientes sobre os campos de uma classe, como é feito, por exemplo, em Java (ClassName.class).

Gostei apenas do QJson para a plataforma Qt , mas ainda não se encaixava perfeitamente no meu entendimento da facilidade de uso gerada pelas bibliotecas mencionadas acima. Então o projeto apareceu, que será discutido aqui.

Pequeno aviso:esses mecanismos não resolverão o problema da interpretação dos dados para você. Tudo o que você pode obter deles é a conversão de dados em um formato mais conveniente para você.

Estrutura do projeto QSerializer


O projeto e os exemplos podem ser visualizados no GitHub ( link para o repositório ). Instruções detalhadas de instalação também são fornecidas lá.

Antecipando o suicídio arquitetônico, farei uma reserva de que essa não é a versão final. O trabalho continuará apesar das pedras abandonadas, mas tendo em conta os desejos.
Dependências estruturais gerais da biblioteca QSerializer

O principal objetivo deste projeto é tornar a serialização usando o formato de dados de fácil compreensão do usuário em C ++ acessível e elementar. A chave para o desenvolvimento e manutenção da qualidade do produto é sua arquitetura. Não excluo que outras formas de implementação possam aparecer nos comentários deste artigo, por isso deixei um pouco de “espaço para a criatividade”. Se você alterar a implementação, poderá adicionar uma nova implementação da interface PropertyKeeper ou alterar os métodos de fábrica para não precisar alterar nada nas funções do QSerializer.

Declaração de campo


Uma maneira de coletar informações de meta-objetos no Qt é descrevê-las no sistema de meta-objetos do próprio Qt. Talvez seja a maneira mais fácil. O MOC gerará todos os metadados necessários no momento da compilação. Você pode chamar o método metaObject no objeto descrito, que retornará uma instância da classe QMetaObject, com a qual temos que trabalhar.

Para declarar os campos a serem serializados, você precisa herdar a classe do QObject e incluir nela a macro Q_OBJECT , para deixar claro ao MOC sobre a qualificação do tipo de classe como base do QObject.

Em seguida, a macro Q_PROPERTY descreve os membros da classe. Vamos chamar a propriedade property descrita em Q_PROPERTY . QSerializer ignorará a propriedade sem o sinalizador USER definido como true.

Por que a bandeira 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; 
};

Para declarar tipos de usuário não padrão no sistema de meta-objetos Qt, sugiro o uso da macro QS_REGISTER , definida em qserializer.h. QS_REGISTER automatiza o processo de registro de variações de tipo. No entanto, você pode usar o método clássico de registrar tipos por meio de qRegisterMetaType < T > (). Para um sistema de meta-objeto, o tipo de classe ( T ) e o ponteiro de classe ( T *) são tipos completamente diferentes; eles terão identificadores diferentes na lista geral 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)
...
}

Espaço para nome QSerializer


Como você pode ver no diagrama UML, o QSerializer contém várias funções para serialização e estruturação. O espaço para nome reflete conceitualmente a essência declarativa do QSerializer. A funcionalidade incorporada pode ser acessada através do nome QSerializer, sem a necessidade de criar um objeto em qualquer lugar do código.

Usando o exemplo de criação de JSON com base no objeto da classe User descrito acima, você só precisa chamar o 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);

E aqui está o JSON resultante:

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

Existem duas maneiras de estruturar um objeto:

  • Se você precisar modificar um objeto
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • Se você precisar obter um novo objeto
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

Mais exemplos e resultados podem ser vistos na pasta de exemplos .

Guardiões


Para organizar a gravação e a leitura convenientes das propriedades declaradas, o QSerializer usa as classes Keepers , cada uma das quais armazena um ponteiro para um objeto (descendente do QObject) e um dos seus QMetaProperty. O próprio QMetaProperty não possui um valor específico, na verdade, é apenas um objeto com uma descrição da classe de propriedade que foi declarada para o MOC. Para ler e escrever, você precisa de um objeto de classe específico, em que essa propriedade é descrita - essa é a principal coisa que você precisa lembrar.

Cada campo serializável durante a serialização é passado para o custodiante do tipo correspondente. Os detentores são necessários para encapsular a funcionalidade de serialização e estruturação para uma implementação específica para um tipo específico de dados descritos. Eu destaquei 4 tipos:

  • QMetaSimpleKeeper - detentor da propriedade com tipos de dados primitivos
  • QMetaArrayKeeper - detentor de propriedades com matrizes de dados primitivos
  • QMetaObjectKeeper - detentor de objetos aninhados
  • QMetaObjectArrayKeeper - detentor de matrizes de objetos aninhados

Fluxo de dados

No coração dos guardiões de dados primitivos está a conversão de informações de JSON / XML para QVariant e vice-versa, porque o QMetaProperty trabalha com o QVariant por padrão.

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

Os detentores de objetos são baseados na transferência de informações do JSON / XML para uma série de outros detentores de serviços e vice-versa. Esses custodiantes trabalham com sua propriedade como um objeto separado, que também pode ter seus próprios custodiantes; sua tarefa é coletar dados serializados do objeto de propriedade e estruturar o objeto de propriedade de acordo com os dados disponíveis.

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

Os detentores implementam a interface PropertyKeeper, da qual a classe abstrata base de detentores é herdada. Isso permite analisar e compor documentos em formato XML ou JSON sequencialmente de cima para baixo, simplesmente descendo as propriedades armazenadas descritas e aprofundando-se à medida que você desce nos objetos incorporados, se houver, nas propriedades descritas, sem entrar em detalhes da implementação.

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

Fábrica do Guardião


Como todos os custodiantes implementam uma interface, todas as implementações ficam ocultas atrás de uma tela conveniente e um conjunto dessas implementações é fornecido pela fábrica da KeepersFactory. Do objeto transferido para a fábrica, você pode obter uma lista de todas as propriedades declaradas por meio de seu QMetaObject, com base na qual o tipo de custodiante é determinado.

Implementação da Fábrica da 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;
}
...


Um recurso importante da fábrica de guardiões é a capacidade de fornecer uma série completa de guardiões para um objeto, e você pode expandir a lista de tipos primitivos suportados editando coleções constantes com identificadores de tipo. Cada série de detentores é um tipo de mapa para propriedades do objeto. Quando um objeto KeepersFactory é destruído, a memória alocada para a série de keepers fornecidos por ele é liberada.

Limitações e comportamento

SituaçãoComportamento
Tentativa de serializar um objeto cujo tipo não é herdado do QObjectErro de compilação
Tipo não declarado ao tentar serialização / estruturaçãoExceção QSException :: UnsupportedPropertyType
Uma tentativa de serializar / estruturar um objeto com um tipo primitivo diferente daquele descrito nas coleções simple_t e array_of_simple_t.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


Na minha opinião, o projeto acabou valendo a pena, porque este artigo foi escrito. Para mim, concluí que não há soluções universais, você sempre tem que sacrificar alguma coisa. Ao desenvolver flexibilidade, em termos de uso, funcionalidade, você reduz a simplicidade e vice-versa.

Não recomendo que você use o QSerializer, meu objetivo é o meu próprio desenvolvimento como programador. Claro, eu também busco o objetivo de ajudar alguém, mas em primeiro lugar - apenas obter prazer. Seja positivo)

All Articles