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

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.
Под псевдонимом 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) };
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/
Добавить комментарий