Почему XML и JSON – плохо, и как сделать хорошо.
Краткое резюме:
- XML, JSON, YAML, SDL – плохо пригодны для описания произвольных иерархий типизированных объектов.
- Но теперь у нас есть альтернативный формат CatML:
- простой,
- интуитивно понятный,
- не допускающий неоднозначности,
- удобный для парсинга,
- кодирующий и строго типизированные данные,
- кодирующий перекрестные ссылки,
- кодирующий глобально именованные объекты и ссылки на них.
- Можно прямо сейчас скачать и использовать его енкодер и декодер для Java, который поддерживает:
- сериализацию объектов
- и DOM-like способ доступа.
- Java-библиотека занимает около 1 тыс. строк и может легко портироваться на любой язык.
Цели
Время от времени нам приходится представлять иерархии объектов в виде текстов, например:- для отладки, чтобы выводить в лог внутренности программы,
- для передачи данных по сети, если религия запрещает бинарные форматы,
- для сохранения документов в файлы (по крайней мере, пока нет более правильного решения)
Аналоги
Какие форматы может предложить нам индус-три-я?- XML – недавний фаворит. Сегодня форсится, пожалуй, только Misrosoft-ом и их приспешниками.
- JSON – набирающий популярность формат, уши которого растут из JavaScript. От XML отличается более вменяемым синтаксисом.
- YAML – самый удобочитаемый из перечисленных форматов. Тем не менее людей понимающих и использующих его очень мало.
- SDL – XML с фигурными скобками.
Критика
Перечисленные форматы имеют несколько проблем, которые делают их плохо пригодными для представления иерархий объектов. Кроме очевидного фатального недостаткаРассмотрим их на паре простых примеров:
Допустим, у нас есть такие структуры данных (псевдокод):
class Point {
int x, y;
}
class Polygon {
String name;
Point[] points;
}
...и такие объекты – полигон с тремя точками:
Polygon name “a test one” points: Point x 42 y 12 Point x 0 y 100 Point x 100 y 100
Как они могут быть представлены в XML
<polygon name="a test one">
<points>
<point x="42" y="12"/>
<point x="0" y="100"/>
<point x="100" y="100"/>
</points>
</polygon>
Они же в JSON
{ “type”: “Polygon”,
“name”: “A test one”,
“points”:[
{ “type”: “Point”, “x”: 42, “y”: 12 },
{ “type”: “Point”, “x”: 0, “y”: 100 },
{ “type”: “Point”, “x”: 100, “y”: 100 },
]
}
И они же в YAML
!Polygon
name: a test one
points:
- !Point
x: 42
y: 100
- !Point
x: 42
y: 100
- !Point
x: 42
y: 100
Сразу видны косяки XML
- Неоднозначность кодирования: поля объектов могут храниться и в виде вложенных нод - «points» или атрибутов «name, x, y» (при этом одни должны быть примитивных типов).
- Еще одна неоднозначность — нода может кодировать и объект “Point” и поле “points”.
- Из примитивных типов данных есть только текст: x=“42”.
- Обилие синтаксического мусора.
- Нет синтаксиса для задания типа объекта. Тип — это просто одно из произвольно назначенных на эту роль полей,
- Обилие синтаксического мусора.
- Как и в XML, один и тот элемент может быть закодирован множеством разных способов, что усложняет парсеры и создает неоднозначности (чтение текстового представления во внутренние объекты и последующая запись создадут два разных текста).
- Как и в JSON, YAML кодирует вложенные друг в друга мапы и массивы, а не объекты, поэтому тип приходится задавать как один из атрибутов. Существуют расширения, позволяющие указывать тип в виде !<Имя типа>. Но эти расширения мало где поддерживаются.
- Текстовые значения не отделяются от объектов, поэтому работают сложные не очевидные правила типизации и множество разных способов искейпинга текста.
Перекрестные ссылки
В первом примере наши объекты составляли дерево. В реальной жизни такое бывает редко, чаше объекты ссылаются друг на друга перекрестными ссылками и эту информацию нельзя терять.Вот еще один пример:
//
// Форма имеет массив кнопок и ссылку на текущую сфокусированную кнопку.
//
class Form{
Button[] buttons;
Button focused;
}
//
// Кнопка имеет текст надписи и ссылку на форму, к которой она принадлежит.
//
class Button{
String caption;
Form form;
}
Допустим у нас есть такой набор объектов:Form.confirmation
buttons:
Button.buttonOk
caption ”OK”
form=confirmation
Button
caption ”Cancel”
form=confirmation
focused=buttonOk
В этом примере я пометил несколько объектов идентификаторами (buttonOk и confirmation), чтобы обозначить ссылки на них из других объектов. Например, форма ссылается своим полем “focused” на одну из кнопок, а кнопки ссылаются на форму своими полями form.Чтобы обозначить, что значением является перекрестная ссылка, я использовал нотацию “=имя”
Как с представлением этих объектов справятся наши подопытные форматы?
В XML нет специального представления для перекрестных ссылок. Там принято использовать пару атрибутов id/idref для связывания объектов.
<Form id='confirmation'>
<form_buttons>
<Button id='buttonOk'>
<button_caption value='OK'/>
<button_form idref='confirmation'/>
</Button>
<Button>
<button_caption value='Cancel'/>
<button_form idref='confirmation'/>
</Button>
</form_buttons>
<form_focused idref='buttonOk'/>
</Form>
Приложение должно самостоятельно обрабатывать эти атрибуты и создавать ссылки.JSON вообще не умеют кодировать перекрестные ссылки, поэтому он отпадает.
YAML имеет синтаксис &refname *refname для установления ссылок, но, к сожалению, парсер превращает ссылки в копии объектов. Поэтому в JSON и YAML, можно воспользоваться подходом XML – ввести особые атрибуты, которые в отдельном проходе внутри приложения будут превращаться в ссылки.
Итог сравнения:
Мейнстрим-языки разметки непригодны для представления иерархий объектов.По следующим причинам:
- Отсутствие представления перекрестных ссылок
- Недостаток типизации примитивных типов (кроме JSON и SDL)
- Отсутствие типизации узлов дерева (Кроме XML и SDL)
- Перегруженный синтаксис, усложняющий инструментарий, делающий трансляцию неоднозначной.
CatML
Как может выглядеть формат, свободный от перечисленных недостатковМы сохраняем объекты, у которых должен быть:
- тип,
- опциональный идентификатор,
- набор полей.
Выберем самый простой синтаксис:
ИмяТипа.необязательный_идентификатор имя_поля1 значение имя_поля2 значение ... имя_поляN значениеНапример
point x 10 y 42Тип поля однозначно выводится из инициализирующего зачения:
| Целое число | знаковое число в десятичной записи | 42 |
| Строка | символы в кавычках с java-искейпингом | ”aaa\n\u0430” |
| Вложенный объект | Описание объекта, при этом имя типа и идентификатор находятся на той же строке, а список полей — на следующих строках с отступом. | Rectangle origin Point x 10 y 20 size Point x 100 y 200 |
| Ссылка на существующий объект | «=» и имя объекта. Null-ссылка обозначается именем "_". |
focused= buttonOk |
| Массив всего перечисленного. | ":" и необязательный идентификатор затем с новой строки с отступом следуют описания элементов массива, если элементами являются объекты, между ними добавляется одна пустая строка. |
names:
"qwer"
"asdf"
points:
Point
x 1
y 2
Point
x 100
y 200
items:
=buttonOk
=buttonCancel
|
По правде говоря, все примеры в этом тексте были даны как раз в этой нотации.
Данный формат:
- Прост
- Однозначен
- Интуитивен
- Строго типизирован
- Позволяет описывать произвольные иерархии объектов, а не только деревья.
- Не имеет зарезервированных слов, использует минимум спецсимволов "=:."
- Имеет префиксную структуру, его парсер будет однопроходным без возвратов.
Внутреннее представление
Если используемый язык программирования имеет хоть какую-то типизацию, появляется вопрос, в каком режиме показывать программе результаты парсинга:Можно создать реальные экземпляры классов приложения, например, если в тексте встретились типы Form, Button, Point, декодер формата создаст экземпляры этих классов и заполнит из поля значениями, присутствующими в файле. Это называется сериализацией и десериализацией.
А можно загрузить разбираемый файл в некие универсальные структуры данных — комбинации словарей и массивов, и позволить приложению обходить и изменять эти структуры данных, получать и дополнять информацию об их типах. Это - загрузка в универсальную модель документа (DOM).
Оба подхода имеют право на жизнь.
Сериализация позволяет с минимальными усилиями сохранять и восстанавливать объекты программы, передавать их по сети и записывать в детализированные логи, но требует чтобы файл содержал только конкретные поддерживаемые приложением типы данных, с жестко заданным набором и типами полей.
Универсальная модель документа (DOM) позволяет загружать и обрабатывать файлы произвольной структуры, но используемые объекты не будут родными для приложения.
Могут существовать и другие решения. Например, сериализация с ограничением состава допустимых типов данных и допустимых полей (из соображения безопасности). Или комбинирование сериализации и универсальной DOM, при котором среди универсальных узлов будут встречаться реальные объекты приложения.
Библиотека использует простой универсальный интерфейс доступа к данным (interface Dom).
В библиотеке присутствуют две реализации этого интерфейса
- VarioDom – универсальная модель документа,
- JavaDom – десериализация с помощью рефлексии.
Пример использования
Используем библиотеку для сериализации/десериализации.// Допустим, у нас есть такие типы данных:
class Point {
int x, y;
}
class Polygon {
String name;
WeakReference<Polygon> peer;
List<Point> points = new ArrayList<Point>();
}
// прочитаем из файла объекты
JavaDom dom = new JavaDom();
Polygon p = (Polygon)CatReader.parseFile(dom, new StringReader(
"Polygon\n"+
"name \"Test text\"\n"+
"points:\n"+
" Point\n"+
" x 1\n"+
" y 42\n"));
// изменим
p.points.add(new Point());
p.peer = new WeakReference<Polygon>(p);
// запишем обратно
CatWriter.encode(dom, p, new OutputStreamWriter(System.out));
Результат работы программыPolygon.L0
name "Test text"
peer=L0
points:
Point
x 1
y 42
Point
x 0
y 0
Используем библиотеку для работы с универсальным DOM.
// Этот объект обеспечивает доступ к метаинформации и данным
VarioDom dom = new VarioDom();
// Парсим текст и формируем универсальные DOM структуры
Object root = CatReader.parseFile(dom, new StringReader(
"Form\n"+
"title\"Test text\"\n"+
"x 42\n"+
"y 10\n"+
"widths:\n"+
" 10\n"+
" 20\n"));
// Получаем тип корневого элемента
StructInfo rootType = dom.getType(root);
System.out.println("object of type " + rootType.getName());
// Получаем список имен полей и их значения
for (int i = 0; i < rootType.getFieldsCount(); i++) {
FieldInfo fieldType = rootType.get(i);
System.out.print("field " + fieldType.getName() + " ->");
for (int j = 0; j < fieldType.getCount(root); j++)
System.out.print(" " + fieldType.get(root, j));
System.out.println();
}
// ...Изменяем поля
rootType.get("title").set(root, 0, "Another title");
// ...Создаем новые типы данных
StructInfo pointType = dom.getOrCreateStruct("Point");
FieldInfo pointX = pointType.getOrCreate("x");
FieldInfo pointY = pointType.getOrCreate("y");
// ...Создаем экземпляры этих типов
Object p = pointType.create();
pointX.set(p, 10);
pointY.set(p, 20);
// К существующим типам можно добавлять новые поля
FieldInfo newField = rootType.getOrCreate("pos");
newField.set(root, p);
// ... и создавать перекрестные ссылки
FieldInfo xrefField = rootType.getOrCreate("peerLink");
xrefField.set(root, new WeakReference(root));
// Закодируем обратно в текст.
CatWriter.encode(dom, root, new OutputStreamWriter(System.out));
Результат работы программыobject of type Form field title -> Test text field x -> 42 field y -> 10 field widths -> 10 20 Form.L0 title "Another title" x 42 y 10 widths: 10 20 pos Point x 10 y 20 peerLink=L0
Глобальные имена
Каждый DOM позволяет давать объектам имена.При сохранении в текстовый формат эти объекты будут помечены этими именами с приписанной в начале точкой.
При чтении текстового формата все имена с точкой будут занесены в DOM, чтобы приложение могло обращаться к ним по именам.
При описании weak-ссылки имя тоже может начинаться с точки, в этом случае имя ищется в DOM, а не в текущем тексте.
Это позволяет разным загружаемым текстовым пакетам ссылаться на объекты друг друга и на глобальные именованные объекты, определенные в приложении.
Тут: BinaryCatML описан бинарный формат, который:
- компактен
- быстр в кодировании и парсинге
- однозначно конвертируется в текстовый CatML и обратно.
Весь код в этом блоге выпускается под лицензией CC0.
No comments:
Post a Comment