Прошло несколько месяцев с тех пор, как я здесь рассказал о своем проекте Qt-based библиотеки для сериализации данных из объектного вида в JSON/XML и обратно.
И как бы я не гордился выстроенной архитектурой, надо признать — реализация получилась, прямо скажем, спорной.
Все это вылилось в масштабную переработку, о результатах которой пойдет речь в этой статье. За подробностями — под кат!
У QSerializer были недостатки, решение которых зачастую становилось еще большим недостатком, вот несколько из них:
Помимо прочего, хотелось иметь возможность у любого объекта сериализоваться «здесь и сейчас», когда для этого приходилось пользоваться огромной обвязкой методов в пространстве имен QSerializer.
QSerializer не был полноценным. Необходимо было придумать решение, при котором бы пользователь не зависел от QObject, была возможность работать со значимыми типами и подешевле.
В комментарии к предыдущей статье, пользователь microla заметил, что можно подумать над применением Q_GADGET.
Достоинства Q_GADGET:
Оперевшись на Q_GADGET, пришлось пересмотреть подход к способам создания JSON и XML на основе задекларированных полей класса. Проблема «дороговизны» проявлялась в первую очередь из за:
Для снижения стоимости я сформулировал следующее требование:
Обойти строгую типизацию С++, усложняющую автоматическую сериализацию, не так просто, и предыдущий опыт это показал. Макросы же могут послужить прекрасным подспорьем для решения такой проблемы (практически вся метаобъектная система Qt построена на макросах), ведь с помощью макросов можно сделать кодогенерацию методов и properties.
Да, зачастую макросы представляют из себя зло в чистом виде — их практически невозможно отлаживать. Написание макроса для генерации кода я мог бы сравнить с надеванием хрустальной туфельки на пятку вашего босса, но сложно — не значит невозможно!
Лирическое отступление о макросах
Сейчас в QSerializer предусмотрено 2 пути для объявления класса как сериализуемого: наследование от класса QSerializer или использование макроса генерации кода QS_CLASS.
Перво наперво необходимо определить макрос Q_GADGET в теле класса, это дает доступ к staticMetaObject, в нем будут храниться сгенерированные макросами properties.
Наследование от QSerializer позволит привести множество сериализуемых объектов к одному типу и сериализовать их скопом.
Класс QSerializer содержит 4 метода-проводника, которые позволят разбирать properties объекта и один виртуальный метод для получения экземпляра QMetaObject:
Q_GADGET не имеет всей метаобъектной обвязки, которую предоставляет Q_OBJECT.
Внутри QSerializer экземпляр staticMetaObject будет представлять класс QSerializer, но никак не производный от него, поэтому при создании QSerializer-based класса необходимо переопределить метод metaObject. Можно добавить макрос QS_SERIALIZER в тело класса и он переопределит метод metaObject за Вас.
А еще использование staticMetaObject вместо хранения экземпляра QMetaObject в каждом объекте экономит 40 байт от размера класса, ну вообще красота!
Если наследоваться по каким-либо причинам не хочется — можно определить в теле сериализуемого класса макрос QS_CLASS, он сгенерирует все необходимые методы вместо наследования их от QSerializer.
Обособленно, в JSON и XML есть 4 вида сериализуемых данных, без которых сериализация в эти форматы не будет полноценной. В таблице приведены виды данных и соответствующие им макросы как способ описания:
Будем считать, что код, который генерируют эти макросы, называется описанием, а макросы, которые его генерируют — описательными.
Принцип генерации описания один — для конкретного поля сгенерировать JSON и XML property и определить методы записи/чтения значений.
Разберем генерацию описания JSON на примере поля примитивного типа данных:
Для поля int digit будет сгенерирован property digit с типом QJsonValue и определены приватные методы записи и чтения — get_json_digit и set_json_digit, они то и станут проводниками для сериализации/десериализации поля digit с использованием JSON.
А вот и генерация описания JSON для сложной структуры:
Сложные объекты — это набор вложенных properties, которые для внешнего класса будут работать как одно «большое» property, потому что такие объекты также будут иметь методы-проводники. Все что для этого нужно сделать — в методах чтения и записи у сложных структур вызывать соответствующий метод-проводник.
Таким образом, мы имеем достаточно простую инфраструктуру для создания сериализуемого класса.
Так, например, можно сделать класс сериализуемым с помощью наследования от QSerializer:
Или так, используя макрос QS_CLASS:
Одиночные поля
Пользовательские или примитивные типы должны предоставлять конструктор по умолчанию.
Коллекции
Класс коллекции должен быть шаблонным и предоставлять методы 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
Предыдущая статья: QSerializer: решение для простой сериализации JSON/XML
Future list
И как бы я не гордился выстроенной архитектурой, надо признать — реализация получилась, прямо скажем, спорной.
Все это вылилось в масштабную переработку, о результатах которой пойдет речь в этой статье. За подробностями — под кат!
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 и использовать этот макрос по отношению к любым другим типам.
Под псевдонимом 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
Добавим еще один класс для полноты картины:
Создание объекта, его наполнение и сериализация:
Получившийся JSON:
Как видите — ничего сверхъестественного, все перечисленное эквивалентно и для XML формата, нужно только заменить метод toJson на toXml.
Более подробные примеры Вы найдете в папке example.
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
Предыдущая статья: QSerializer: решение для простой сериализации JSON/XML
Future list
- Существенное удешевление (можно сделать еще дешевле)
- Компактность
- Работа со значимыми типами
- Элементарное описание сериализуемых данных
- Поддержка любых шаблонных коллекций, предоставляющих методы clear, at, size и append. Даже собственных
- Полная изменяемость коллекций при десериализации
- Поддержка всех популярных примитивных типов
- Поддержка любого пользовательского типа, описанного с использованием QSerializer
- Отсутствие необходимости регистрировать пользовательские типы