QSerializer: Lösung für die einfache JSON / XML-Serialisierung

Hallo Habr!

Ich dachte, dass es irgendwie unfair ausfällt - in Java, C #, Go, Python usw. Es gibt Bibliotheken für die komfortable Serialisierung von Objektdaten in jetzt modisches JSON und XML, aber in C ++ haben sie es entweder vergessen oder wollten es nicht oder brauchten es nicht wirklich, oder es ist alles kompliziert oder vielleicht alles zusammen. Also habe ich beschlossen, dieses Problem zu beheben.

Alle Details wie gewohnt unter dem Schnitt.

Bild

Hintergrund


Wieder einmal entschied ich mich für das nächste Haustierprojekt, dessen Kern der Client-Server-Austausch war, während RaspberryPi der Lieblingsserver vieler war. Unter anderem interessierte mich das Problem der Erstellung von "Speicherpunkten" - damit ich als Teil des Prototyps den Status des Objekts so einfach wie möglich speichern konnte, bevor ich es beim nächsten Start verließ und wiederherstellte. Aufgrund meiner unvernünftigen Feindseligkeit gegenüber Python und meiner sehr herzlichen Haltung gegenüber Qt habe ich mich für Qt & C ++ entschieden. Das Schreiben von Kursen und Spaghetti-Funktionen zum Parsen von JSON ist immer noch ein Vergnügen. Ich brauchte eine universelle und gleichzeitig einfache Lösung für mein Problem. "Wir müssen es herausfinden", sagte ich mir.

Zunächst ein wenig zu den Begriffen:
Bei der Serialisierung wird eine Datenstruktur in eine Folge von Bits übersetzt. Die Umkehrung der Serialisierungsoperation ist die Deserialisierungsoperation (Strukturierungsoperation) - die Wiederherstellung des Anfangszustands der Datenstruktur aus einer Bitsequenz.
Go verfügt über ein sehr nützliches "natives" Codierungs- / JSON- Paket , mit dem Sie die Serialisierung des Objekts mithilfe der Marshal-Methode und die umgekehrte Strukturierung mit Unmarshal abschließen können (aufgrund dieser Bibliothek hatte ich zuerst eine falsche Vorstellung vom Marshalling, aber Desine sperare qui hic intras ). . Nach den Konzepten dieses Pakets fand ich eine weitere Bibliothek für Java - GSON , die sich als sehr angenehmes Produkt herausstellte. Es war eine Freude, sie zu verwenden.

Ich überlegte, was ich an diesen Bibliotheken mag und kam zu dem Schluss, dass es ihre Benutzerfreundlichkeit war. Flexible Funktionalität und alles in einem Aufruf. Für die Serialisierung in JSON war es ausreichend, die toJson-Methode aufzurufen und ein serialisierbares Objekt an sie zu übergeben. C ++ selbst verfügt jedoch standardmäßig nicht über die richtigen Metaobjektfunktionen, um genügend Informationen zu den Feldern einer Klasse bereitzustellen, wie dies beispielsweise in Java (ClassName.class) der Fall ist.

Ich mochte QJson nur für die Qt-Plattform , aber es passte immer noch nicht ganz zu meinem Verständnis der Benutzerfreundlichkeit, die durch die oben genannten Bibliotheken generiert wurde. So erschien das Projekt, das hier diskutiert wird.

Kleiner Haftungsausschluss:Solche Mechanismen lösen das Problem der Dateninterpretation für Sie nicht. Alles, was Sie von ihnen erhalten können, ist die Konvertierung von Daten in eine für Sie bequemere Form.

QSerializer-Projektstruktur


Das Projekt und die Beispiele können auf GitHub angezeigt werden ( Link zum Repository ). Dort finden Sie auch detaillierte Installationsanweisungen.

Im Vorgriff auf den Selbstmord in der Architektur werde ich einen Vorbehalt machen, dass dies nicht die endgültige Version ist. Die Arbeiten werden trotz der verlassenen Steine ​​fortgesetzt, aber unter Berücksichtigung der Wünsche.
Allgemeine strukturelle Abhängigkeiten der QSerializer-Bibliothek

Das Hauptziel dieses Projekts ist es, die Serialisierung im benutzerfreundlichen Datenformat in C ++ zugänglich und elementar zu machen. Der Schlüssel zur Qualitätsentwicklung und -wartung des Produkts ist seine Architektur. Ich schließe nicht aus, dass in den Kommentaren zu diesem Artikel andere Implementierungsmethoden erscheinen, deshalb habe ich ein wenig „Raum für Kreativität“ gelassen. Wenn Sie die Implementierung ändern, können Sie entweder eine neue Implementierung der PropertyKeeper-Schnittstelle hinzufügen oder die Factory-Methoden ändern, sodass Sie nichts an den QSerializer-Funktionen ändern müssen.

Felddeklaration


Eine Möglichkeit, Metaobjektinformationen in Qt zu sammeln, besteht darin, sie im Metaobjektsystem von Qt selbst zu beschreiben. Vielleicht ist dies der einfachste Weg. MOC generiert zur Kompilierungszeit alle erforderlichen Metadaten. Sie können die metaObject-Methode für das beschriebene Objekt aufrufen, die eine Instanz der QMetaObject-Klasse zurückgibt, mit der wir arbeiten müssen.

Um zu serialisierende Felder zu deklarieren, müssen Sie die Klasse von QObject erben und das Makro Q_OBJECT einbinden , um dem MOC klar zu machen, ob der Klassentyp als Basis von QObject qualifiziert werden soll.

Als Nächstes beschreibt das Makro Q_PROPERTY die Mitglieder der Klasse. Wir werden die in Q_PROPERTY beschriebene Eigenschaft property aufrufen . QSerializer ignoriert Eigenschaften, ohne dass das USER-Flag auf true gesetzt ist.

Warum USER Flagge
, , 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; 
};

Um nicht standardmäßige Benutzertypen im Qt- Metaobjektsystem zu deklarieren, empfehle ich die Verwendung des Makros QS_REGISTER , das in qserializer.h definiert ist. QS_REGISTER automatisiert den Prozess der Registrierung von Typvariationen . Sie können jedoch die klassische Methode zum Registrieren von Typen mit qRegisterMetaType < T > () verwenden. Bei einem Metaobjektsystem sind der Klassentyp ( T ) und der Klassenzeiger ( T *) völlig unterschiedliche Typen, sie haben unterschiedliche Bezeichner in der allgemeinen Liste der Typen.

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

QSerializer-Namespace


Wie Sie dem UML-Diagramm entnehmen können, enthält QSerializer eine Reihe von Funktionen zur Serialisierung und Strukturierung. Der Namespace spiegelt konzeptionell die deklarative Essenz von QSerializer wider. Auf die eingebettete Funktionalität kann über den Namen QSerializer zugegriffen werden, ohne dass irgendwo im Code ein Objekt erstellt werden muss.

Am Beispiel des Erstellens von JSON basierend auf dem oben beschriebenen Objekt der Benutzerklasse müssen Sie nur die QSerializer :: toJson-Methode aufrufen:

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

Und hier ist der resultierende JSON:

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

Es gibt zwei Möglichkeiten, ein Objekt zu strukturieren:

  • Wenn Sie ein Objekt ändern müssen
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • Wenn Sie ein neues Objekt benötigen
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

Weitere Beispiele und Ausgaben finden Sie im Beispielordner .

Bewahrer


Um das bequeme Schreiben und Lesen deklarierter Eigenschaften zu organisieren, verwendet QSerializer Keepers- Klassen , in denen jeweils ein Zeiger auf ein Objekt (QObject-Nachkomme) und eine seiner QMetaProperty gespeichert ist. QMetaProperty selbst ist nicht von besonderem Wert, sondern nur ein Objekt mit einer Beschreibung der Eigenschaftsklasse, die für das MOC deklariert wurde. Zum Lesen und Schreiben benötigen Sie ein bestimmtes Klassenobjekt, in dem diese Eigenschaft beschrieben wird. Dies ist die Hauptsache, an die Sie sich erinnern müssen.

Jedes serialisierbare Feld während der Serialisierung wird an die Depotbank des entsprechenden Typs übergeben. Keepers werden benötigt, um die Serialisierungs- und Strukturierungsfunktionen für eine bestimmte Implementierung für einen bestimmten Typ der beschriebenen Daten zu kapseln. Ich habe 4 Typen hervorgehoben:

  • QMetaSimpleKeeper - Property Keeper mit primitiven Datentypen
  • QMetaArrayKeeper - Property Keeper mit Arrays primitiver Daten
  • QMetaObjectKeeper - Bewahrer verschachtelter Objekte
  • QMetaObjectArrayKeeper - Verwalter von Arrays verschachtelter Objekte

Datenstrom

Das Herzstück primitiver Datenverwalter ist die Konvertierung von Informationen von JSON / XML nach QVariant und umgekehrt, da QMetaProperty standardmäßig mit QVariant arbeitet.

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

Objekthalter basieren auf der Übertragung von Informationen von JSON / XML an eine Reihe anderer Bewahrer und umgekehrt. Solche Depotbanken arbeiten mit ihrem Eigentum als separates Objekt, das auch eigene Depotbanken haben kann. Ihre Aufgabe besteht darin, serialisierte Daten vom Immobilienobjekt zu sammeln und das Immobilienobjekt gemäß den verfügbaren Daten zu strukturieren.

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

Keepers implementieren die PropertyKeeper-Schnittstelle, von der die abstrakte Basisklasse der Keepers geerbt wird. Auf diese Weise können Sie Dokumente im XML- oder JSON-Format nacheinander von oben nach unten analysieren und zusammenstellen, indem Sie einfach die beschriebenen gespeicherten Eigenschaften durchgehen und tiefer in die eingebetteten Objekte (falls vorhanden) in den beschriebenen Eigenschaften eintauchen, ohne auf Details der Implementierung einzugehen.

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

Wächterfabrik


Da alle Depotbanken eine Schnittstelle implementieren, sind alle Implementierungen hinter einem praktischen Bildschirm verborgen, und eine Reihe dieser Implementierungen wird von der KeepersFactory-Factory bereitgestellt. Von dem an die Factory übertragenen Objekt können Sie über sein QMetaObject eine Liste aller deklarierten Eigenschaften abrufen, anhand derer der Typ der Depotbank bestimmt wird.

KeepersFactory Factory-Implementierung
    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;
}
...


Ein wichtiges Merkmal der Guardian-Factory ist die Möglichkeit, eine vollständige Reihe von Guardians für ein Objekt bereitzustellen. Sie können die Liste der unterstützten primitiven Typen erweitern, indem Sie konstante Sammlungen mit Typ-IDs bearbeiten. Jede Reihe von Keepern ist eine Art Karte für Eigenschaften des Objekts. Wenn ein KeepersFactory-Objekt zerstört wird, wird der für die von ihm bereitgestellte Reihe von Keepern zugewiesene Speicher freigegeben.

Einschränkungen und Verhalten

SituationVerhalten
Versuchen Sie, ein Objekt zu serialisieren, dessen Typ nicht von QObject geerbt wurdeKompilierungsfehler
Nicht deklarierter Typ beim Versuch der Serialisierung / StruturisierungQSException :: UnsupportedPropertyType-Ausnahme
Ein Versuch, ein Objekt mit einem anderen primitiven Typ als dem in den Sammlungen simple_t und array_of_simple_t beschriebenen zu serialisieren / strukturieren.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


Meiner Meinung nach hat sich das Projekt gelohnt, da dieser Artikel geschrieben wurde. Für mich kam ich zu dem Schluss, dass es keine universellen Lösungen gibt, man muss immer etwas opfern. Durch die Entwicklung flexibler Funktionen in Bezug auf Nutzung und Funktionalität können Sie die Einfachheit zunichte machen und umgekehrt.

Ich fordere Sie nicht auf, QSerializer zu verwenden. Mein Ziel ist vielmehr meine eigene Entwicklung als Programmierer. Natürlich verfolge ich auch das Ziel, jemandem zu helfen, aber in erster Linie - nur Freude zu haben. Sei positiv)

All Articles