Комментарии 15
Как-то очень сложно, хотя и довольно симпатично…
Почему бы не определить у классов метод [toJSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior), который заменяет в ссылках указатели на строковые uuid, а при десериализации резолвить эти uuid обратно?
Другими словами, будет аналогичный десериализатор этой схемы на другом объектно-ориентированном языке.
Несколько замечаний.
А не лучше ли было вместо полного преобразования графа объектов использовать второй параметр в JSON.stringify?
Вот это выражение —
this._ctorToName[val.constructor]
— полагается на то что toString для разных конструкторов будет выдавать разные значения. А это может быть и не так: одного неудачного декоратора достаточно чтобы половина "конструкторов" оказалась с одним и тем же toString… Надежнее использовать Map.
Вместо
__uuid
лучше использовать символ. Мало ли кому еще придет в голову идея назначать__uuid
объектам...
Методы serialize и deserialize тоже лучше делать символами. Нельзя же так просто добавлять левые методы стандартным объектам.
- Метод deserialize должен быть статическим! Иначе получаются некрасивые костыли вроде того который вы показали для даты.
В итоге сериализацию даты я вижу как-то вот так:
Date.prototype[SerializerSymbols.serialize] = function () {
return this.toISOString();
};
Date[SerializerSymbols.deserialize] = function (val) {
return new Date(val);
};
При этом представляется, что в подобном случае лучше разбить шаг «десериализация-связывание» на два. Т.е. мы на первом шаге просто десериализуем поступившие данные из DTO и лишь затем создаем бизнес-объекты. В этом случае links будет содержать id вершин, а вершины будут поступать в отдельном словаре вершин из которых мы будем доставать их по id.
Да, это чуть сложнее и требует работы как на стороне сервера (где мы преобразуем бизнес-объекты в DTO), так и на стороне клиента. Но выглядеть будет точно проще.
Была похожая задача, но решил просто: хотя у нас есть дерево, может даже с циклами, но оно же как-то представлено уже в памяти? Не на уровне "элемент циклически связан с другим", а на уровне "квадратик соединен стрелочкой". Вот это состояние и сериализуем бинарно. И обратно читаем. А уж определять связи, циклы и взаимоотношения — это задача алгоритмов поиска, анализа. Сериализация же для хранения и передачи, нефиг ей пастись в циклах )
Для таких задач (в смысле вообще работа со сложными графами объектов на клиенте, а не только сериализация) хорошо подходят normalizr + mobx-state-tree:
- Преобразуем развесистое JSON-дерево от сервера в плоские "таблицы" с помощью normalizr
- Скармливаем их mobx-state-tree в качестве снапшота состояния
- Ну а обратно сериализовать можно совсем просто:
function serialize(data) {
const visited = new Set();
return JSON.stringify(data, (key, value) => {
if (isObject(value) && isInteger(value.id)) {
if (visited.has(value)) {
return { id: value.id };
}
visited.add(value);
return value;
}
return value;
});
}
Плюсы:
- Где удобно, можно работать с данными как с таблицей (например применить снапшот или смерджить)
- Где удобно — как с "живыми" объектами (добавить/убрать связи, изменить поля)
- Автоматический рендеринг изменений (за счет MobX)
- Проверка типов данных (в том числе в runtime)
Минусы:
- Нужно прописывать схемы данных на клиенте (но мы ведь все равно так или иначе это сделаем)
Я бы рекомендовал сериализовать во что-то типа такого:
{
"user": {
"jin": {
"name": "Jin",
"karma": 100500,
"birthday": "1984-08-04",
"articles": [ "foo" ]
}
},
"bot": {
"google": {
"name": "GoogleBot",
"articles": [ "bar" ]
}
},
"article": {
"foo": {
"name": "Fooooo",
"author": [ "user" , "jin" ]
},
"bar": {
"name": "Barrrr",
"authors": [ "bot" , "google" ]
}
}
}
По уровням:
- тип сущности
- идентификатор сущности
- данные сущности
Данные могут быть:
- число:
100500
- строка:
"GoogleBot"
- момент времени:
"1894-08-04"
- типизированная ссылка на сущность (только идентификатор, ведь тип заранее известен):
"foo"
- полиморфная ссылка на сущность (имя типа и идентификатор, когда ссылаться может на разные типы):
[ "user" , "jin" ]
- список типизированных ссылок:
[ "foo" , "bar" ]
- список полиморфных ссылок:
[ [ "user" , "jin" ] , [ "bot" , "google" ] ]
- словарь (ключи — строки, значения — любые типы данных):
{ "foo": 1 , "bar": 2 }
Преимущества:
- простота и ясность
- скорость обработки (не надо парсить ключи и тп)
- нормализация (нет дублирования данных)
- компактность (не храним тип, если он и так известен)
- возможность ссылаться на любые сущности, в том числе на внешние (не представленные в конкретном срезе графа)
- совместимость со статически типизированными языками (данные разных типов лежат в разных "букетах")
Классовая сериализация на JavaScript с поддержкой циклических ссылок