QSerializer: solusi untuk serialisasi JSON / XML sederhana

Halo, Habr!

Saya pikir entah bagaimana hasilnya tidak adil - di Jawa, C #, Go, Python, dll. Ada perpustakaan untuk serialisasi data objek yang nyaman ke dalam JSON dan XML yang sekarang modis, tetapi di C ++ mereka lupa, atau tidak mau, atau tidak benar-benar membutuhkannya, atau semuanya rumit, atau mungkin semuanya bersamaan. Jadi saya memutuskan untuk memperbaiki hal ini.

Semua detail, seperti biasa, di bawah potongan.

gambar

Latar Belakang


Sekali lagi, saya memutuskan untuk mengambil proyek hewan peliharaan berikutnya, yang intinya adalah pertukaran klien-server, sementara server favorit banyak orang adalah RaspberryPi. Antara lain, saya tertarik pada masalah menciptakan "save points" - jadi saya bisa sesederhana mungkin, sebagai bagian dari prototipe, menyelamatkan keadaan objek sebelum keluar dan memulihkan pada awal berikutnya. Karena permusuhan saya yang tidak masuk akal terhadap Python dan sikap saya yang sangat hangat terhadap Qt, saya memilih Qt & C ++. Menulis kelas dan fungsi spageti untuk mem-parsing JSON masih menyenangkan, saya membutuhkan solusi universal dan sekaligus mudah untuk masalah saya. "Kita harus mencari tahu," kataku pada diri sendiri.

Pertama, sedikit tentang ketentuan:
Serialisasi adalah proses menerjemahkan struktur data ke dalam urutan bit. Kebalikan dari operasi serialisasi adalah operasi deserialisasi (penataan) - pemulihan keadaan awal struktur data dari urutan bit.
Go memiliki paket enkode / json "asli" yang sangat berguna yang memungkinkan untuk serialisasi objek secara lengkap menggunakan metode Marshal dan membalikkan struktur menggunakan Unmarshal (karena perpustakaan ini, saya pertama kali memiliki gagasan yang salah tentang pengurutan , tetapi Desine sperare qui hic intras ) . Mengikuti konsep paket ini, saya menemukan perpustakaan lain untuk Java - GSON , yang ternyata merupakan produk yang sangat menyenangkan, sangat menyenangkan untuk menggunakannya.

Saya merenungkan apa yang saya sukai dari perpustakaan ini dan sampai pada kesimpulan bahwa itu adalah kemudahan penggunaannya. Fungsionalitas yang fleksibel dan semuanya dalam satu panggilan, untuk serialisasi di JSON cukup untuk memanggil metode toJson dan meneruskan objek serial ke dalamnya. Namun, C + + itu sendiri, secara default, tidak memiliki kemampuan metaobject yang tepat untuk memberikan informasi yang cukup tentang bidang kelas, seperti yang dilakukan, misalnya, di Jawa (ClassName.class).

Saya hanya menyukai QJson untuk platform Qt , tapi tetap saja itu tidak sesuai dengan pemahaman saya tentang kemudahan penggunaan yang dihasilkan oleh perpustakaan yang disebutkan di atas. Maka proyek muncul, yang akan dibahas di sini.

Penafian kecil:mekanisme seperti itu tidak akan menyelesaikan masalah interpretasi data untuk Anda. Yang bisa Anda dapatkan dari mereka adalah konversi data menjadi bentuk yang lebih nyaman bagi Anda.

Struktur Proyek QSerializer


Proyek dan contoh-contohnya dapat dilihat di GitHub ( tautan ke repositori ). Petunjuk pemasangan terperinci juga diberikan di sana.

Mengantisipasi bunuh diri arsitektural, saya akan membuat reservasi bahwa ini bukan versi final. Pekerjaan akan terus berlanjut terlepas dari batu-batu yang terbengkalai, tetapi telah mengijinkan keinginan.
Ketergantungan struktural umum dari perpustakaan QSerializer

Tujuan utama dari proyek ini adalah membuat serialisasi menggunakan format data yang mudah digunakan pengguna dalam C ++ yang dapat diakses dan dasar. Kunci dari pengembangan kualitas dan pemeliharaan produk adalah arsitekturnya. Saya tidak mengesampingkan bahwa cara implementasi lain mungkin muncul dalam komentar di artikel ini, jadi saya meninggalkan sedikit โ€œruang untuk kreativitasโ€. Jika Anda mengubah implementasi, Anda bisa menambahkan implementasi baru dari antarmuka PropertyKeeper atau mengubah metode pabrik sehingga Anda tidak perlu mengubah apa pun dalam fungsi QSerializer.

Deklarasi lapangan


Salah satu cara untuk mengumpulkan informasi meta-objek di Qt adalah dengan menggambarkannya dalam sistem meta-objek Qt itu sendiri. Mungkin ini cara termudah. MOC akan menghasilkan semua metadata yang diperlukan pada waktu kompilasi. Anda dapat memanggil metode metaObject pada objek yang dideskripsikan, yang akan mengembalikan turunan dari kelas QMetaObject, yang harus kita kerjakan.

Untuk mendeklarasikan bidang yang akan diserialkan, Anda perlu mewarisi kelas dari QObject dan menyertakan makro Q_OBJECT di dalamnya , untuk memperjelas kepada MOC tentang kualifikasi jenis kelas sebagai dasar dari kelas QObject.

Selanjutnya, makro Q_PROPERTY menjelaskan anggota kelas. Kami akan memanggil properti properti yang dijelaskan dalam Q_PROPERTY . QSerializer akan mengabaikan properti tanpa flag USER disetel ke true.

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

Untuk mendeklarasikan tipe pengguna non-standar dalam sistem meta-objek Qt, saya sarankan menggunakan makro QS_REGISTER , yang didefinisikan dalam qserializer.h. QS_REGISTER mengotomatiskan proses mendaftarkan variasi jenis. Namun, Anda dapat menggunakan metode klasik untuk mendaftarkan tipe dengan qRegisterMetaType < T > (). Untuk sistem objek-meta, tipe kelas ( T ) dan pointer kelas ( T *) adalah tipe yang sama sekali berbeda, mereka akan memiliki pengidentifikasi yang berbeda dalam daftar tipe umum.

#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


Seperti yang Anda lihat dari diagram UML, QSerializer berisi sejumlah fungsi untuk serialisasi dan penataan. Namespace secara konseptual mencerminkan esensi deklaratif dari QSerializer. Fungsionalitas yang disematkan dapat diakses melalui nama QSerializer, tanpa perlu membuat objek di mana pun dalam kode.

Menggunakan contoh membangun JSON berdasarkan objek kelas Pengguna yang dijelaskan di atas, Anda hanya perlu memanggil metode 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);

Dan inilah JSON yang dihasilkan:

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

Ada dua cara untuk menyusun objek:

  • Jika Anda perlu memodifikasi objek
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • Jika Anda perlu mendapatkan objek baru
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

Lebih banyak contoh dan output dapat dilihat di folder contoh .

Penjaga


Untuk mengatur penulisan dan pembacaan properti yang dideklarasikan dengan nyaman, QSerializer menggunakan kelas Keepers , yang masing-masing menyimpan pointer ke objek (QObject descendant) dan salah satu dari QMetaProperty-nya. QMetaProperty sendiri tidak memiliki nilai tertentu, bahkan hanya objek dengan deskripsi kelas properti yang dideklarasikan untuk MOC. Untuk membaca dan menulis, Anda memerlukan objek spesifik dari kelas di mana properti ini dijelaskan - ini adalah hal utama yang perlu diingat.

Setiap bidang serializable selama serialisasi diteruskan ke penjaga dari jenis yang sesuai. Penjaga diperlukan untuk merangkum fungsi serialisasi dan penataan untuk implementasi spesifik untuk tipe spesifik dari data yang diuraikan. Saya menyoroti 4 jenis:

  • QMetaSimpleKeeper - penjaga properti dengan tipe data primitif
  • QMetaArrayKeeper - penjaga properti dengan array data primitif
  • QMetaObjectKeeper - penjaga objek bersarang
  • QMetaObjectArrayKeeper - penjaga array objek bersarang

Aliran data

Inti dari pemelihara data primitif adalah konversi informasi dari JSON / XML ke QVariant dan sebaliknya, karena QMetaProperty bekerja dengan QVariant secara default.

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

Penjaga objek didasarkan pada transfer informasi dari JSON / XML ke serangkaian penjaga lain dan sebaliknya. Penjaga tersebut bekerja dengan properti mereka sebagai objek yang terpisah, yang juga dapat memiliki penjaga sendiri, tugas mereka adalah mengumpulkan data bersambung dari objek properti dan menyusun objek objek sesuai dengan data yang tersedia.

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

Penjaga mengimplementasikan antarmuka PropertyKeeper, dari mana kelas abstrak dasar dari penjaga diwarisi. Ini memungkinkan Anda untuk mem-parsing dan menyusun dokumen dalam format XML atau JSON secara berurutan dari atas ke bawah, cukup turun ke properti tersimpan yang dijelaskan dan pergi lebih dalam saat Anda turun ke objek tertanam, jika ada, di properti yang dijelaskan, tanpa masuk ke rincian implementasi.

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

Pabrik Pelindung


Karena semua penjaga menerapkan satu antarmuka, semua implementasi disembunyikan di balik layar yang nyaman, dan satu set implementasi ini disediakan oleh pabrik KeepersFactory. Dari objek yang ditransfer ke pabrik, Anda bisa mendapatkan daftar semua properti yang dideklarasikan melalui QMetaObject, berdasarkan tipe kustodian yang ditentukan.

Implementasi Pabrik 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;
}
...


Fitur utama dari pabrik pelindung adalah kemampuan untuk menyediakan serangkaian pelindung yang lengkap untuk suatu objek, dan Anda dapat memperluas daftar tipe primitif yang didukung dengan mengedit koleksi konstan dengan pengidentifikasi tipe. Setiap rangkaian penjaga adalah semacam peta untuk properti untuk objek. Ketika objek KeepersFactory dihancurkan, memori yang dialokasikan untuk serangkaian penjaga yang disediakan olehnya dibebaskan.

Keterbatasan dan Perilaku

SituasiTingkah laku
Mencoba membuat serial objek yang tipenya tidak diwarisi dari QObjectKesalahan kompilasi
Jenis tidak dideklarasikan ketika mencoba serialisasi / struturisasiQSException :: Pengecualian UnsupportedPropertyType
Upaya untuk membuat cerita bersambung / struktur objek dengan tipe primitif berbeda dari yang dijelaskan dalam koleksi simple_t dan array_of_simple_t.QSException::UnsupportedPropertyType. , โ€” ,
JSON/XML
propertyes, JSON/XMLpropertyes . โ€” propertyes
JSON propertyQSException


Menurut saya, proyek itu ternyata bermanfaat, karena artikel ini ditulis. Bagi saya sendiri, saya menyimpulkan bahwa tidak ada solusi universal, Anda selalu harus mengorbankan sesuatu. Dengan mengembangkan fleksibilitas, dalam hal penggunaan, fungsionalitas, Anda membunuh kesederhanaan, dan sebaliknya.

Saya tidak mendesak Anda untuk menggunakan QSerializer, tujuan saya adalah pengembangan saya sendiri sebagai seorang programmer. Tentu saja, saya juga mengejar tujuan membantu seseorang, tetapi pada awalnya - hanya mendapatkan kesenangan. Menjadi positif)

All Articles