Как стать автором
Обновить

QSerializer: решение для простой сериализации JSON/XML

Программирование *C++ *XML *Qt *
Привет, Хабр!

Подумалось мне, что как-то несправедливо получается — у Java, С#, Go, Python и т.д. есть библиотеки для комфортной сериализации объектных данных в модные нынче JSON и XML, а в С++ то ли забыли, то ли не захотели, то ли и не особо надо, то ли сложно все это, а может и все вместе. Так что я решил это дело исправить.

Все подробности, как обычно, под катом.



Предыстория


Вновь я решил заняться очередным pet-проектом, суть которого заключалась в клиент-серверном обмене, сервером же должна была выступать любимая многими RaspberryPi. Помимо прочего, меня интересовал вопрос создания «точек сохранения» — так я мог бы максимально просто, в рамках прототипа, сохранить состояние объекта перед выходом и восстановиться при следующем запуске. По причине необоснованной неприязни к Python и моему весьма теплому отношению к Qt, я выбрал именно Qt & C++. Писать классы и спагетти-функции разбора JSON — то еще удовольствие, нужно было какое-то универсальное и в то же время легкое решение моей проблемы. «Надо разбираться», — сказал я себе.

Для начала немного о терминах:
Сериализация— процесс перевода какой-либо структуры данных в последовательность битов. Обратной к операции сериализации является операция десериализации (структуризации) — восстановление начального состояния структуры данных из битовой последовательности.
В Go есть очень полезный «родной» пакет encoding/json, позволяющий производить полную сериализацию объекта методом Marshal и обратную структуризацию с помощью Unmarshal (из-за этой библиотеки у меня сначала сложилось не совсем верное понятие о маршалинге, но Desine sperare qui hic intras). Придерживаясь концепций этого пакета, я нашел еще одну библиотеку для Java — GSON, которая оказалась весьма приятным продуктом, использовать ее было сплошным удовольствием.

Я размышлял над тем, что мне нравится в этих библиотеках, и пришел к выводу, что это их простота в использовании. Гибкий функционал и все в одном вызове, для сериализации в JSON достаточно было вызвать метод toJson и передать в него сериализуемый объект. Однако сам по себе C++ по умолчанию не обладает должными метаобъектными возможностями, для того чтобы предоставить достаточно информации о полях класса, как это сделано, например, в Java (ClassName.class).

Под платформу Qt мне приглянулось только QJson, но все равно она не совсем укладывалась в мое понимание простоты использования, сформированное вышеупомянутыми библиотеками. Так появился проект, о котором здесь пойдет речь.

Небольшой дисклеймер: подобные механизмы не решат за вас задачу интерпретации данных. Все, что вы можете от них получить — это конвертацию данных в более удобный для вас вид.
NOTE: Вышла вторая статья о QSerializer

Cтруктура проекта QSerializer


Проект и примеры можно посмотреть на GitHub (ссылка на репозиторий). Там же расписана подробная инструкция по установке.

Предвкушая архитектурное самоубийство, сделаю оговорку, что это не финальная версия. Работа будет вестись и дальше, невзирая на брошенные камни, но сделав поправку на пожелания.
Общие структурные зависимости библиотеки QSerializer

Главная преследуемая цель этого проекта — сделать сериализацию с использованием user-frendly формата данных в С++ доступной и элементарной. Залог качественного развития и поддержания продукта — его архитектура. Я не исключаю, что в комментариях к этой статье могут появиться другие способы реализации, поэтому оставил немного «пространства для творчества». В случае изменения реализации можно либо добавить новую реализацию интерфейса PropertyKeeper, либо изменить методы фабрики так, что в функциях QSerializer ничего менять не придется.

Декларация полей


Одним из способов сбора метаобъектной информации в Qt является ее описание в метаобъектной системе самого Qt. Пожалуй, это самый простой способ. MOC сгенерит все необходимые метаданные на этапе компиляции. У описанного объекта можно будет вызвать метод metaObject, который вернет экземпляр класса QMetaObject, с которым нам и предстоит работать.

Для объявления подлежащих сериализации полей нужно унаследовать класс от QObject и включить в него макрос Q_OBJECT, дабы дать понять MOC о квалификации типа класса как базового от QObject.

Дальше макросом Q_PROPERTY описать члены класса. Мы будем называть описанное в Q_PROPERTY свойство property. QSerializer будет игнорировать property без флага USER равного true.

Зачем флаг 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; 
};

Для декларации нестандартных пользовательских типов в метаобъектной системе 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 на основе объекта вышеописанного класса User надо только вызвать метод 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);

Больше примеров и выходных данных можно увидеть в папке example.

Хранители


Для организации удобной записи и чтения декларированных свойств QSerializer использует классы-хранители (Keepers), каждый из них хранит указатель на объект (наследник QObject) и одну из его QMetaProperty. Сама по себе QMetaProperty не представляет особой ценности, по сути это лишь объект с описанием property класса, которая была задекларирована для MOC. Для чтения и записи нужен конкретный объект класса, где описано это свойство — это главное что нужно запомнить.

Каждое сериализуемое поле в процессе сериализации передается в хранитель соответствующего типа. Хранители нужны, чтобы инкапсулировать функционал сериализации и структуризации для конкретной реализации под определенный тип описываемых данных. Я выделил 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 в серии других хранителей и обратно. Такие хранители работают со своим property как с отдельным объектом, который также может иметь своих хранителей, их задача состоит в сборе сериализованных данных из объекта property и структурировании объекта property по имеющимся данным.

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

Хранители реализуют интерфейс PropertyKeeper, от которого унаследован базовый абстрактный класс хранителей. Это позволяет разбирать и составлять документы в формате XML или JSON последовательно сверху вниз, просто спускаясь по описанным хранимым свойствам и углубляясь по мере спуска во вложенные объекты, если таковые имеются в описанных propertyes, при этом не вдаваясь в детали реализации.

Интерфейс 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. У переданного в фабрику объекта можно получить список всех задекларированных propertyes через его QMetaObject, на основе которых определяется тип хранителя.

Реализация фабрики 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;
}
...


Ключевой особенностью фабрики хранителей является возможность предоставления полной серии хранителей для объекта, а расширить список поддерживаемых примитивных типов можно отредактировав константные коллекции с идентификаторами типов. Каждая серия хранителей представляет собой своеобразную карту по propertyes для объекта. При разрушении объекта KeepersFactory — память, выделенная под предоставленные ею серии хранителей, высвобождается.

Ограничения и поведение

Ситуация Поведение
Попытка сериализации объекта, тип которого не унаследован от QObject Ошибка компиляции
Незадекларированный тип при попытке сериализации/стркутуризации Исключение QSException::UnsupportedPropertyType
Попытка сериализации/структуризации объекта с примитивным типом отличающимся от описанных в коллекциях simple_t и array_of_simple_t. Исключение QSException::UnsupportedPropertyType. Используйте стандартно закрепленные типы, а если очень нужно — можно добавить нужный вам примитив, но никаких гарантий
В JSON/XML есть лишние поля Лишние поля игнорируются
В объекте есть propertyes, которых нет в JSON/XML Лишние propertyes игнорируются. Если структуризация сопровождается созданием нового объекта — проигнорированные propertyes будут равны дефолтным значениям или задающимся в конструкторе по умолчанию
Несоответствие типа описанных данных поля в JSON и property объекта Исключение QSException

В заключение


На мой взгляд, проект получился стоящим, потому эта статья и написана. Для себя я сделал вывод, что универсальных решений не бывает, всегда приходится чем-то жертвовать. Разрабатывая гибкую, с точки зрения использования, функциональность, вы убиваете простоту, и наоборот.

Я не призываю вас использовать QSerializer, моей целью служит скорее собственное развитие как программиста. Разумеется я преследую и цель помочь кому-то, но в первую очередь — просто получение удовольствия. Будьте позитивными)
Теги:
Хабы:
Всего голосов 9: ↑8 и ↓1 +7
Просмотры 3.7K
Комментарии Комментарии 9