вторник, 14 декабря 2021 г.

[prog.c++] Обновление json_dto с очередным уроком на тему собственных просчетов

Есть у нас небольшой и пока еще не очень старый проект json_dto. Ему чуть больше пяти лет, но уже даже на таком небольшом временном интервале проявляется важный эффект: когда проект развивается, то ты неизбежно сталкиваешься с собственными просчетами, допущенными когда-то в прошлом.

Выпущенная давеча версия 0.3.0 как раз один такой просчет и устраняет.

Суть в том, что в json_dto есть функции mandatory, optional, optional_no_default, которые связывают подлежащее (де)сериализации поле объекта с информацией о том, как это поле должно выглядеть в JSON-е:

struct message_t
{
   std::string m_from;
   std::int64_t m_when;
   std::string m_text;

   // Entry point for json_dto.
   template<typename Json_Io>
   void json_io(Json_Io & io)
   {
      io & json_dto::mandatory("from", m_from)
         & json_dto::mandatory("when", m_when)
         & json_dto::optional("text", m_text);
   }
};

Связывание происходит посредством создания объекта-binder-а в котором хранится имя поля в JSON, ссылка на само поле и, возможно, еще какая-то информация (вроде валидаторов десериализуемых значений).

А объект-binder -- это экземпляр шаблонного класса, одним из шаблонных параметров которого является т.н. manopt_policy (mandatory-optional policy). Свойство manopt_policy определяет то, является ли поле обязательным или опциональным, есть ли у него значение по умолчанию (которое можно и не сериализовать в JSON).

Соответственно, в json_dto есть несколько реализаций manopt_policy: одна реализация отвечает за поддержку обязательных полей, вторая -- за поддержку опциональных с наличием дефолтного значения, третья -- за поддержку опциональных без дефолтного значения.

Этого набора реализаций manopt_policy до сих пор хватало.

Но недавно мы наступили на грабли, которые сами же заложили в json_dto пять лет назад.

Грабли эти состояли в том, что manopt_policy обрабатывал только ситуации с отсутствием значения при десериализации. Но вот если поле присутствовало, но имело значение null, то тут manopt_policy игнорировался, а на помощь приходила свободная функция-шаблон set_attr_value_null. Т.е. при десериализации выполнялось что-то вроде:

const auto it = object.FindMember( binder_data.field_name() );
if( object.MemberEnd() != it ) // Значение в JSON присутствует.
{
   const auto & value = it->value;

   if( !value.IsNull() ) // И это не null, поэтому можно читать значение.
   {
      binder_data.reader_writer().read(
            binder_data.field_for_deserialization(), value );
   }
   else // А вот null обрабатывается особым образом.
   {
      set_attr_null_value(
            binder_data.field_for_deserialization() );
   }
}
else // Значения нет, тут в дело вступает manopt_policy.
{
   binder_data.manopt_policy().on_field_not_defined(
         binder_data.field_for_deserialization() );
}

Наступили мы на эти грабли когда потребовалось создать новый manopt_policy, который бы трактовал null особым способом (в этом случае поле должно было получить дефолтное значение). Тот факт, что обработка null велась без задействования manopt_policy, не позволял нам получить простого в реализации и использовании решения для такой ситуации.

Исправление тривиальное и, на данный момент, выглядящее очевидным: обработка null-а теперь так же отдается на откуп manopt_policy:

const auto it = object.FindMember( binder_data.field_name() );
if( object.MemberEnd() != it ) // Значение в JSON присутствует.
{
   const auto & value = it->value;

   if( !value.IsNull() ) // И это не null, поэтому можно читать значение.
   {
      binder_data.reader_writer().read(
            binder_data.field_for_deserialization(), value );
   }
   else // Теперь null обрабатывается через manopt_policy.
   {
      binder_data.manopt_policy().on_null(
            binder_data.field_for_deserialization() );
   }
}
else // Значения нет, тут в дело вступает manopt_policy.
{
   binder_data.manopt_policy().on_field_not_defined(
         binder_data.field_for_deserialization() );
}

И основной вопрос, который я задаю сейчас самому себе, выглядит так: "Если это настолько тривиально и очевидно, то почему мы не додумались до этого пять лет назад?"

PS. Если бы мне нужно было подвести какую-то мораль под этой историей, то она была бы какой-то такой: если вам повезет сделать собственный проект с нуля, а потом много лет работать над этим проектом, то вы неизбежно столкнетесь с собственными ошибками/просчетами, допущенными когда-то давным-давно. И, т.к. это будут именно что ваши собственные ошибки/просчеты, то винить в этом вы сможете только самого себя.

PPS. Мне повезло с проектами, которые были сделаны с нуля и развивались в течении многих лет, что это привело к следующему мнению: "классность" решения -- это функция, обратно пропорциональная времени. Принятое вами сегодня решение может казаться "классным", но спустя 2-3 года оно уже может быть "решение как решение", а лет через десять -- откровенно "дырявым".

6 комментариев:

sv комментирует...

Мне интересно сама необходимость всех этих инструментов, которые что-то генерируют. В своей жизни я встречался с абсолютно разными json-ами: с такими, где числа представляют из себя hex-строку, с такими, где нужные поля разбросаны по сотне разных уровней вложенности, даже с такими где внутри json лежал другой json в виде строки. И по моему, самый простой способ разобрать любой json это код вида
Message parseJson(const Json &json) {
Message result;
if (json["field1"] == null) {
result.field1 = json["field1"];
} else {
...
}
}
Зачем нужны все эти автоматические генераторы? Если я воспользуюсь каким-либо генератором, наподобие Вашего (но не обязательно им), мне придется долго и мучительно разбираться, как туда добавить нужную фишку, типа распаковки шестнадцатеричного числа, в то время как в обычной функции я могу просто написать код. Тоесть все, что мне нужно сделать, можно сделать посредством обычной функции. Что это, если не обобщенный код? Почему люди забывают, что любой код на любом языке программирования уже сам по себе является обобщенным, зачем придумывать все эти обертки, шаблоны, которые не обобщают код, а наоборот, сильно ограничивают его?

eao197 комментирует...

@sv

> Зачем нужны все эти автоматические генераторы?

Затем, что есть люди, которые не хотят по 100500 раз повторять ручками код вида

if(json["field1"] == null) { result.field1=some_value; } else {...}

Ну и не генератор у нас, вообще-то говоря.

> Что это, если не обобщенный код?

ХЗ что, но вряд ли это обобщенный код.

sv комментирует...

> Затем, что есть люди, которые не хотят по 100500 раз повторять ручками код вида
Можно вынести в отдельную функцию например. Преимущество функции в том, что ее всегда можно заменить, если она перестанет удовлетворять требованиям

eao197 комментирует...

@sv

> Можно вынести в отдельную функцию например.

Ну вот мы и вынесли.

sv комментирует...

Ну да, посмотрел, у вас боле менее нормально. Просто недавно видел доклад тоже про парсинг json, и до сих пор под впечатлением. Тем не менее, в целом, даже с текущей реализацией, вопросы все равно актуальны

eao197 комментирует...

@sv

У нас простая история: когда-то давно начали использовать RapidJSON для работы с JSON-ом, кода приходилось писать ну очень уж много, сделали свою тонкую обертку, объем писанины сократился в разы.

Так что вопросы не понятны от слова совсем. Нравится кому-то пердолиться с ручным разбором JSON-а, так не проблема, как угодно и сколько угодно. Мы просто пойдем другим путем.