QSerializer: حل لتسلسل JSON / XML البسيط

مرحبا يا هابر!

اعتقدت بطريقة ما أنها تتحول بشكل غير عادل - في Java ، C # ، Go ، Python ، إلخ. هناك مكتبات لإجراء تسلسل مريح لبيانات الكائن إلى JSON و XML المألوفين الآن ، ولكن في C ++ إما أنهم نسوا أو لم يرغبوا في ذلك أو لم يحتاجوا إليها حقًا ، أو أنها كلها معقدة ، أو ربما كلها معًا. لذلك قررت إصلاح هذا الشيء.

كل التفاصيل ، كالمعتاد ، تحت القطع.

صورة

خلفية


مرة أخرى ، قررت أن أتناول مشروع الحيوانات الأليفة التالي ، الذي كان جوهره تبادل العملاء والخوادم ، بينما كان الخادم المفضل للعديد من RaspberryPi. من بين أمور أخرى ، كنت مهتمًا بمسألة إنشاء "نقاط حفظ" - لذا يمكنني أن أبسط حالة الكائن ، في إطار النموذج الأولي ، في حالة النموذج قبل الخروج والاسترداد في البداية التالية. نظرًا لعدائي غير المعقول لبايثون وموقفي الحار جدًا تجاه Qt ، اخترت Qt & C ++. كانت دروس الكتابة ووظائف السباغيتي لتحليل JSON لا تزال ممتعة ، كنت بحاجة إلى بعض الحل الشامل وفي نفس الوقت لمشكلتي. قلت لنفسي: "علينا معرفة ذلك".

أولاً ، القليل من المصطلحات:
التسلسل هو عملية ترجمة هيكل البيانات إلى سلسلة من البتات. عكس عملية التسلسل هي عملية إزالة التسلسل (الهيكلة) - استعادة الحالة الأولية لبنية البيانات من تسلسل بت.
يحتوي Go على حزمة ترميز / json "أصلية" مفيدة للغاية تتيح لك إكمال تسلسل الكائن باستخدام طريقة Marshal والبنية العكسية باستخدام Unmarshal (بسبب هذه المكتبة ، كان لدي فكرة خاطئة عن التنظيم ، ولكن Desine sperare qui hic intras ) . باتباع مفاهيم هذه الحزمة ، وجدت مكتبة أخرى لـ Java - GSON ، والتي تبين أنها منتج لطيف للغاية ، كان من دواعي سروري استخدامها.

فكرت في ما يعجبني في هذه المكتبات وتوصلت إلى استنتاج مفاده أن سهولة استخدامها. وظائف مرنة وكلها في مكالمة واحدة ، للتسلسل في JSON كان كافياً لاستدعاء طريقة toJson وتمرير الكائن القابل للتسلسل إليه. ومع ذلك ، فإن C ++ نفسها ، بشكل افتراضي ، ليس لديها إمكانات metaobject مناسبة لتوفير معلومات كافية حول حقول فئة ، كما هو الحال ، على سبيل المثال ، في Java (ClassName.class).

لقد أحببت QJson فقط لمنصة Qt ، ولكن لا يزال لا يتناسب تمامًا مع فهمي لسهولة الاستخدام الناتجة عن المكتبات المذكورة أعلاه. لذلك ظهر المشروع الذي سيتم مناقشته هنا.

إخلاء مسؤولية صغير:لن تحل هذه الآليات مشكلة تفسير البيانات بالنسبة لك. كل ما يمكنك الحصول عليه منهم هو تحويل البيانات إلى شكل أكثر ملاءمة لك.

هيكل مشروع QSerializer


يمكن الاطلاع على المشروع والأمثلة على GitHub ( رابط إلى المستودع ). كما يتم إعطاء تعليمات التثبيت التفصيلية هناك.

توقعًا للانتحار المعماري ، سأبدي تحفظًا على أن هذه ليست النسخة النهائية. سيستمر العمل على الرغم من الأحجار المهجورة ، ولكن تم تخصيص رغبات.
تبعيات هيكلية عامة لمكتبة QSerializer

الهدف الرئيسي من هذا المشروع هو جعل التسلسل باستخدام تنسيق بيانات سهل الاستخدام في C ++ يمكن الوصول إليه وأساسي. مفتاح تطوير جودة المنتج وصيانته هو هندسته المعمارية. أنا لا أستبعد أن طرق التنفيذ الأخرى قد تظهر في التعليقات على هذه المقالة ، لذلك تركت القليل من "مساحة للإبداع". إذا قمت بتغيير التطبيق ، يمكنك إما إضافة تطبيق جديد لواجهة PropertyKeeper أو تغيير طرق المصنع بحيث لا تضطر إلى تغيير أي شيء في وظائف QSerializer.

إعلان ميداني


تتمثل إحدى طرق جمع معلومات الكائنات الوصفية في Qt في وصفها في نظام الكائنات الوصفية في Qt نفسها. ربما هذه هي أسهل طريقة. ستنشئ MOC جميع البيانات الوصفية اللازمة في وقت الترجمة. يمكنك استدعاء طريقة metaObject على الكائن الموصوف ، والذي سيعيد نسخة من فئة QMetaObject ، والتي علينا العمل معها.

لإعلان تسلسل الحقول ، تحتاج إلى وراثة الفئة من QObject وتضمين ماكرو Q_OBJECT فيه ، لتوضيح الأمر لوزارة الثقافة حول تأهيل نوع الفصل على أنه النوع الأساسي من QObject.

بعد ذلك ، يصف الماكرو Q_PROPERTY أعضاء الفئة. سنتصل بممتلكات العقار الموضحة في Q_PROPERTY . سيتجاهل QSerializer الخاصية بدون تعيين علامة USER إلى true.

لماذا علم المستخدم
, , 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; 
};

للإعلان عن أنواع المستخدمين غير القياسية في نظام الكائنات الوصفية Qt ، أقترح استخدام الماكرو QS_REGISTER ، المحدد في qserializer.h. يعمل QS_REGISTER على أتمتة عملية تسجيل اختلافات النوع. ومع ذلك ، يمكنك استخدام الطريقة الكلاسيكية لتسجيل الأنواع باستخدام qRegisterMetaType < T > (). بالنسبة لنظام الكائنات الوصفية ، يعد نوع الفئة ( T ) ومؤشر الفئة ( T *) أنواعًا مختلفة تمامًا ؛ سيكون لديهم معرفات مختلفة في القائمة العامة للأنواع.

#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


كما ترى من مخطط UML ، يحتوي QSerializer على عدد من الوظائف للتسلسل والهيكلة. تعكس مساحة الاسم من الناحية المفاهيمية الجوهر التعريفي لـ QSerializer. يمكن الوصول إلى الوظيفة المضمنة من خلال اسم QSerializer ، دون الحاجة إلى إنشاء كائن في أي مكان في التعليمات البرمجية.

باستخدام مثال بناء JSON استنادًا إلى كائن فئة المستخدم الموصوف أعلاه ، ما عليك سوى استدعاء طريقة 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);

وهنا JSON الناتج:

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

هناك طريقتان لبناء كائن:

  • إذا كنت بحاجة إلى تعديل كائن
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • إذا كنت بحاجة إلى الحصول على كائن جديد
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

يمكن رؤية المزيد من الأمثلة والإخراج في مجلد الأمثلة .

حفظة


لتنظيم الكتابة والقراءة الملائمة للخصائص المعلنة ، يستخدم QSerializer فئات Keepers ، كل منها يخزن مؤشرًا إلى كائن (QObject سليل) وأحد QMetaProperty. QMetaProperty نفسها ليست ذات قيمة معينة ، في الواقع إنها مجرد كائن له وصف لفئة الخصائص التي تم تعريفها لـ MOC. للقراءة والكتابة ، تحتاج إلى كائن فئة معين ، حيث يتم وصف هذه الخاصية - هذا هو الشيء الرئيسي الذي تحتاج إلى تذكره.

يتم تمرير كل حقل قابل للتسلسل أثناء التسلسل إلى أمين الحفظ من النوع المقابل. يحتاج Keepers إلى تغليف وظائف التسلسل والهيكلة لتطبيق معين لنوع معين من البيانات الموصوفة. أبرزت 4 أنواع:

  • QMetaSimpleKeeper - حارس الممتلكات مع أنواع البيانات البدائية
  • QMetaArrayKeeper - حارس الممتلكات مع صفائف من البيانات البدائية
  • QMetaObjectKeeper - حارس الكائنات المتداخلة
  • QMetaObjectArrayKeeper - حارس صفائف الكائنات المتداخلة

تدفق المعلومات

يعتبر تحويل المعلومات من JSON / XML إلى QVariant والعكس صحيح في صميم حماة البيانات البدائية ، لأن QMetaProperty يعمل مع QVariant بشكل افتراضي.

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

تعتمد حافظة الكائنات على نقل المعلومات من JSON / XML إلى سلسلة من الحافظين الآخرين والعكس صحيح. يعمل هؤلاء الحافظون مع ممتلكاتهم ككائن منفصل ، والذي يمكن أن يكون له أيضًا أمناء حراسة خاصة بهم ، ومهمتهم هي جمع البيانات المتسلسلة من كائن الملكية وتنظيم كائن الملكية وفقًا للبيانات المتاحة.

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 بتطبيق واجهة PropertyKeeper ، والتي يتم من خلالها توارث فئة التجريد الأساسية للحافظة. يتيح لك هذا تحليل المستندات وإنشائها بتنسيق XML أو JSON بالتسلسل من الأعلى إلى الأسفل ، والذهاب ببساطة إلى الخصائص المخزنة الموصوفة والتعمق أكثر عندما تنزل إلى الكائنات المضمنة ، إن وجدت ، في الخصائص الموصوفة ، دون الدخول في تفاصيل التنفيذ.

واجهة 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;
};

مصنع الجارديان


نظرًا لأن جميع الأوصياء ينفذون واجهة واحدة ، فإن جميع عمليات التنفيذ مخفية خلف شاشة مريحة ، ويتم توفير مجموعة من هذه التطبيقات من قبل مصنع KeepersFactory. من الكائن الذي تم نقله إلى المصنع ، يمكنك الحصول على قائمة بجميع الممتلكات المعلنة من خلال QMetaObject ، بناءً على نوع أمين الحفظ الذي يتم تحديده.

تنفيذ مصنع المصنع
    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;
}
...


السمة الرئيسية لمصنع الوصي هي القدرة على توفير سلسلة كاملة من الأوصياء لكائن ما ، ويمكنك توسيع قائمة الأنواع البدائية المدعومة عن طريق تحرير مجموعات ثابتة بمعرفات النوع. كل سلسلة من الحراس هي نوع من الخريطة لخصائص الكائن. عند إتلاف كائن KeepersFactory ، يتم تحرير الذاكرة المخصصة لسلسلة الحافظين التي يوفرها.

القيود والسلوك

موقفسلوك
محاولة إجراء تسلسل للكائن الذي لم يتم توريث نوعه من QObjectخطأ في التحويل
نوع غير معلن عند محاولة التسلسل / struturizationQSException :: استثناء UnsupportedPropertyType
محاولة لتسلسل / بناء كائن بنوع بدائي مختلف عن ذلك الموصوف في مجموعتي simple_t و array_of_simple_t.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


في رأيي ، تبين أن المشروع جدير بالاهتمام ، لأنه تمت كتابة هذه المقالة. بالنسبة لي ، خلصت إلى أنه لا توجد حلول عالمية ، يجب عليك دائمًا التضحية بشيء ما. من خلال تطوير المرونة من حيث الاستخدام والوظائف ، فإنك تقتل البساطة والعكس صحيح.

أنا لا أحثك ​​على استخدام QSerializer ، هدفي هو تطوير مبرمج لي. بالطبع ، أسعى أيضًا إلى هدف مساعدة شخص ما ، ولكن في المقام الأول - مجرد الحصول على المتعة. كن ايجابيا)

All Articles