Почему 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