QSerializer умер, да здравствует QSerializer

от автора

Прошло несколько месяцев с тех пор, как я здесь рассказал о своем проекте Qt-based библиотеки для сериализации данных из объектного вида в JSON/XML и обратно.
И как бы я не гордился выстроенной архитектурой, надо признать — реализация получилась, прямо скажем, спорной.

Все это вылилось в масштабную переработку, о результатах которой пойдет речь в этой статье. За подробностями — под кат!
image

QSerializer умер

У QSerializer были недостатки, решение которых зачастую становилось еще большим недостатком, вот несколько из них:

  • Очень дорого (сериализация, содержание хранителей свойств на куче, контроль времени жизни хранителей и т.д.)
  • Работа только с QObject-based классами
  • Вложенные «сложные» объекты и их коллекции должны так же являться QObject-based
  • Невозможность дополнять коллекции при десериализации
  • Только теоретически бесконечная вкладываемость
  • Отсутствие возможности работать со значимыми типами «сложных» объектов, по причине запрета на копирование у QObject
  • Необходимость обязательной регистрации типов в метаобъектной системе Qt
  • Типичные «библиотечные» проблемы вроде проблем с линковкой и переносимостью между платформами

Помимо прочего, хотелось иметь возможность у любого объекта сериализоваться «здесь и сейчас», когда для этого приходилось пользоваться огромной обвязкой методов в пространстве имен QSerializer.

Да здравствует QSerializer!

QSerializer не был полноценным. Необходимо было придумать решение, при котором бы пользователь не зависел от QObject, была возможность работать со значимыми типами и подешевле.
В комментарии к предыдущей статье, пользователь microla заметил, что можно подумать над применением Q_GADGET.

Достоинства Q_GADGET:

  • Не накладывает ограничений на копирование
  • Имеет статический экземпляр QMetaObject для доступа к properties

Оперевшись на Q_GADGET, пришлось пересмотреть подход к способам создания JSON и XML на основе задекларированных полей класса. Проблема «дороговизны» проявлялась в первую очередь из за:

  • Большого размера класса-хранителя (по меньшей мере 40 байт)
  • Выделение кучи под новые сущности хранителей для каждого property и контроль их TTL

Для снижения стоимости я сформулировал следующее требование:

Наличие в каждом сериализуемом объекте методов-проводников для сериализации/десериализации всех properties класса и наличие для каждого property методов чтения и записи значений с использованием отведенного для этого property формата

Макросы

Обойти строгую типизацию С++, усложняющую автоматическую сериализацию, не так просто, и предыдущий опыт это показал. Макросы же могут послужить прекрасным подспорьем для решения такой проблемы (практически вся метаобъектная система Qt построена на макросах), ведь с помощью макросов можно сделать кодогенерацию методов и properties.
Да, зачастую макросы представляют из себя зло в чистом виде — их практически невозможно отлаживать. Написание макроса для генерации кода я мог бы сравнить с надеванием хрустальной туфельки на пятку вашего босса, но сложно — не значит невозможно!

Лирическое отступление о макросах

Макрос — это просто набор лексем, и думать о макросах мы должны именно с точки зрения текста, который после разворачивания должен стать «компилируемым» текстом (токеном). Поэтому макросы можно рассматривать как правила для составления блоков текста.

Декларация класса

Сейчас в QSerializer предусмотрено 2 пути для объявления класса как сериализуемого: наследование от класса QSerializer или использование макроса генерации кода QS_CLASS.

Перво наперво необходимо определить макрос Q_GADGET в теле класса, это дает доступ к staticMetaObject, в нем будут храниться сгенерированные макросами properties.
Наследование от QSerializer позволит привести множество сериализуемых объектов к одному типу и сериализовать их скопом.

Класс QSerializer содержит 4 метода-проводника, которые позволят разбирать properties объекта и один виртуальный метод для получения экземпляра QMetaObject:

QJsonValue toJson() const void fromJson(const QJsonValue &) QDomNode toXml() const void fromXml(const QDomNode &) virtual const QMetaObject * metaObject() const 

Q_GADGET не имеет всей метаобъектной обвязки, которую предоставляет Q_OBJECT.
Внутри QSerializer экземпляр staticMetaObject будет представлять класс QSerializer, но никак не производный от него, поэтому при создании QSerializer-based класса необходимо переопределить метод metaObject. Можно добавить макрос QS_SERIALIZER в тело класса и он переопределит метод metaObject за Вас.
А еще использование staticMetaObject вместо хранения экземпляра QMetaObject в каждом объекте экономит 40 байт от размера класса, ну вообще красота!

Если наследоваться по каким-либо причинам не хочется — можно определить в теле сериализуемого класса макрос QS_CLASS, он сгенерирует все необходимые методы вместо наследования их от QSerializer.

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

Обособленно, в JSON и XML есть 4 вида сериализуемых данных, без которых сериализация в эти форматы не будет полноценной. В таблице приведены виды данных и соответствующие им макросы как способ описания:

Вид данных Описание Макрос
поле обычное поле примитивного типа (различные числа, строки, флаги) QS_FIELD
коллекция набор значений примитивных типов данных QS_COLLECTION
объект сложная структура из полей или других сложных структур QS_OBJECT
коллекция объектов набор из сложных структур данных одного типа QS_COLLECTION_OBJECTS

Будем считать, что код, который генерируют эти макросы, называется описанием, а макросы, которые его генерируют — описательными.
Принцип генерации описания один — для конкретного поля сгенерировать JSON и XML property и определить методы записи/чтения значений.

Разберем генерацию описания JSON на примере поля примитивного типа данных:

/* Create JSON property and methods for primitive type field*/ #define QS_JSON_FIELD(type, name)                                                                Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)                       private:                                                                                         QJsonValue get_json_##name() const {                                                             QJsonValue val = QJsonValue::fromVariant(QVariant(name));                                    return val;                                                                              }                                                                                            void set_json_##name(const QJsonValue & varname){                                                name = varname.toVariant().value<type>();                                                }    ... int digit; QS_JSON_FIELD(int, digit)   

Для поля int digit будет сгенерирован property digit с типом QJsonValue и определены приватные методы записи и чтения — get_json_digit и set_json_digit, они то и станут проводниками для сериализации/десериализации поля digit с использованием JSON.

Как это происходит?

В макросе под псевдонимом name лежит слово digit, два символа решетки (‘##’) конкатинируют слово digit с предстоящей последовательностью символов — так создаются методы.
Под псевдонимом type скрывается слово int. Представьте, что вместо type в сгенерированном коде будет написано int и все станет на свои места. Это дает возможность в макросе привести значение лежащее в QVariant к типу int и использовать этот макрос по отношению к любым другим типам.

А вот и генерация описания JSON для сложной структуры:

/* Generate JSON-property and methods for some custom class */ /* Custom type must be provide methods fromJson and toJson */ #define QS_JSON_OBJECT(type, name)     Q_PROPERTY(QJsonValue name READ get_json_##name WRITE set_json_##name)     private:     QJsonValue get_json_##name() const {         QJsonObject val = name.toJson();         return QJsonValue(val);     }     void set_json_##name(const QJsonValue & varname) {         if(!varname.isObject())         return;         name.fromJson(varname);     }  ... SomeClass object; QS_JSON_OBJECT(SomeClass, object) 

Сложные объекты — это набор вложенных properties, которые для внешнего класса будут работать как одно «большое» property, потому что такие объекты также будут иметь методы-проводники. Все что для этого нужно сделать — в методах чтения и записи у сложных структур вызывать соответствующий метод-проводник.

Создание класса

Таким образом, мы имеем достаточно простую инфраструктуру для создания сериализуемого класса.
Так, например, можно сделать класс сериализуемым с помощью наследования от QSerializer:

class SerializableClass : public QSerializer { Q_GADGET QS_SERIALIZER QS_FIELD(int, digit) QS_COLLECTION(QList, QString, strings) };

Или так, используя макрос QS_CLASS:

class SerializableClass { Q_GADGET QS_CLASS QS_FIELD(int, digit) QS_COLLECTION(QList, QString, strings) };

Пример сериализации в JSON

Добавим еще один класс для полноты картины:

class CustomType : public QSerializer { Q_GADGET QS_SERIALIZER QS_FIELD(int, someInteger) QS_FIELD(QString, someString) };  class SerializableClass : public QSerializer { Q_GADGET QS_SERIALIZER QS_FIELD(int, digit) QS_COLLECTION(QList, QString, strings) QS_OBJECT(CustomType, someObject) QS_COLLECTION_OBJECTS(QVector, CustomType, objects) };

Создание объекта, его наполнение и сериализация:

SerializableClass serializable; serializable.someObject.someString = "ObjectString"; serializable.someObject.someInteger = 99999; for(int i = 0; i < 3; i++) {     serializable.digit = i;     serializable.strings.append(QString("list of strings with index %1").arg(i));     serializable.objects.append(serializable.someObject); } QJsonObject json = serializable.toJson(); 

Получившийся JSON:

{     "digit": 2,     "objects": [         {             "someInteger": 99999,             "someString": "ObjectString"         },         {             "someInteger": 99999,             "someString": "ObjectString"         },         {             "someInteger": 99999,             "someString": "ObjectString"         }     ],     "someObject": {         "someInteger": 99999,         "someString": "ObjectString"     },     "strings": [         "list of strings with index 0",         "list of strings with index 1",         "list of strings with index 2"     ] } 

Как видите — ничего сверхъестественного, все перечисленное эквивалентно и для XML формата, нужно только заменить метод toJson на toXml.
Более подробные примеры Вы найдете в папке example.

Ограничения

Одиночные поля
Пользовательские или примитивные типы должны предоставлять конструктор по умолчанию.

Коллекции
Класс коллекции должен быть шаблонным и предоставлять методы clear, at, size и append. Вы можете использовать собственные коллекции при соблюдении условий. Коллекции Qt, удовлетворяющие этим условиям: QVector, QStack, QList, QQueue.

Версии Qt
Минимальная версия Qt 5.5.0
Минимальная протестированная версия Qt 5.9.0
Максимально протестированная версия Qt 5.15.0
NOTE: вы можете поучавствовать в тестировании и проверить QSerializer на более ранних версиях Qt

Итог

При переработке QSerializer, я совершенно не ставил перед собой задачу уменьшить его в разы. Однако его объем снизился с 9 файлов до 1, что также снизило его сложность. Сейчас QSerializer больше не являет собой библиотеку в привычном нам виде, сейчас — это просто заголовочный файл, который достаточно включить в проект и получить весь функционал для комфортной сериализации/десериализации. Разработка началась еще в марте, придумывалась хитрая архитектура и проект обрастал зависимостями, костылями, переписывался с 0 несколько раз. И все для того, чтобы в конечном итоге превратиться в небольшой файлик.
Спрашивая себя: «Стоил ли он потраченных на него усилий?», я отвечу: «Да, стоил». Я уже успел опробовать его на своих боевых проектах и результат меня порадовал.

Links
GitHub: ссылка
Последний релиз: v1.1

Future list

  • Существенное удешевление (можно сделать еще дешевле)
  • Компактность
  • Работа со значимыми типами
  • Элементарное описание сериализуемых данных
  • Поддержка любых шаблонных коллекций, предоставляющих методы clear, at, size и append. Даже собственных
  • Полная изменяемость коллекций при десериализации
  • Поддержка всех популярных примитивных типов
  • Поддержка любого пользовательского типа, описанного с использованием QSerializer
  • Отсутствие необходимости регистрировать пользовательские типы

ссылка на оригинал статьи https://habr.com/ru/post/515094/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *