Недавно я внес незначительный вклад в проект с открытым исходным кодом, который использовал схему GraphQL в качестве входных данных и генерировал жизнеспособные диалекты SQL для различных баз данных. Поскольку в прошлом я довольно много работал с GraphQL, мне понравилась идея конкретизировать GraphQL API, который включает определения типов, и генерировать функции TypeScript, которые вызывали SQL для выполнения базовых операций CRUD. Это похоже на то, как ORM сопоставляет классы Java с операторами SQL или как генераторы кода на основе Swagger предоставляют структуру, созданную из определения REST API.

GraphQL имеет простой, но расширяемый способ декларативного определения типов данных. Эти типы данных используются для определения и проверки структуры вызовов HTTP POST на сервер, где телом POST является JSON. Также имеется поддержка директив, которые позволяют вносить изменения в схему, чтобы сообщить генератору языка (или механизму проверки), как сопоставлять типы GraphQL с другими типами языков. Недостатком является то, что директивы привязывают схему GraphQL к определенному языку или хранилищу данных.

Дежавю снова и снова

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

  • Объектно-реляционное отображение (ORM)
  • Генерация кода UML CASE
  • Чванство
  • XML-схема / привязка Java PODO
  • Семантическое моделирование объектов
  • Кастор
  • Spring Roo

Определение типов данных и создание языковых отображений являются основной частью этих технологий, поэтому проблемы такого типа возникают постоянно. Это заставило меня задуматься, можно ли абстрагироваться от общей концепции от конкретных деталей реализации. Далее следует общее описание того, как может выглядеть такая абстракция и как она будет действовать.

Примитивные и составные типы данных

Тип - это определение именованной структуры данных. Данные типа должны соответствовать определению. В данном случае я использую слово «структура» для обозначения примитивных типов данных и составных типов данных.

Примитивные типы данных

Они соответствуют данным, хранящимся в памяти атомарно. Большинство языков поддерживают следующее:

  • Целое число
  • Десятичный
  • Плавать
  • Характер
  • Логический

Некоторые из них могут включать Валюта и Дата.

Составные типы данных

Составной тип определяется как контейнер для примитивных и других составных типов. У них разные имена:

  • структура (C ++)
  • множество
  • сложный тип (схема XML)
  • Обычный объект старых данных (PODO, разные языки)

Программист определяет их, дает определению имя и использует их экземпляры в коде. В отличие от примитивов, которые определяются языком, составные типы являются «пользовательскими» типами.

Аспекты универсальной системы типов

Схема UTS, которая определяет примитивные и составные типы, не требует реализации. В этом смысле это чисто концептуальное и неспецифическое описание типов, используемых в предметной области. Таким образом, целое число в UTS - это целое число без ограничения диапазона; аналогично, строка - это непрерывный набор символов без фиксированной длины. Итак, схема UTS описывает типы данных, используемые в домене, но этого недостаточно для реализации самостоятельно.

Как и GraphQL, в UTS были бы упрощены ограничения количества элементов: один к одному, один ко многим и ноль ко многим. Эти ограничения могут быть ужесточены на выбранном целевом языке (ах) реализации.

Так что в этом хорошего?

  • как концептуальная модель, она не увязла в деталях реализации и, следовательно, фокусируется на том, что важно для специалиста в предметной области (например, бизнес-аналитика)
  • как набор определений типов он может быть представлен на других языках, которые могут вводить собственные ограничения. Дополнительные ограничения могут быть определены разработчиком для конкретного языка через конфигурацию.

Основные компоненты

С этого момента я буду использовать термин «диалект» вместо «язык», а также «субдиалект» для обозначения разновидности языка. В идеях, описанных ниже, много нечеткости, поскольку эта статья задумана как часть размышлений, а не как подробное руководство по реализации со всеми проработанными (или даже учтенными) проблемами.

Базовое определение

Это ядро ​​UTS: схема, определяющая именованные типы с помощью примитивов и композитов. Он может иметь сходство со схемой определения типа GraphQL. Поскольку он определяет граф отношений между типами, он мало чем отличается от схемы реляционной базы данных, где таблица и столбцы следуют структуре Таблица для каждого типа.

Наследование типов напрямую не поддерживается, потому что UTS не задуман как способ классификации типов в терминах отношений «есть-а». UTS может поддерживать расширение типа в качестве удобного сокращения для уменьшения избыточности в определениях, но расширение не будет подразумевать классификацию, хотя реализация на диалекте может интерпретировать это как таковое.

Расширения домена

Средства расширения языка UTS позволили бы такие вещи, как определения дополнительных примитивных типов или диапазоны значений для типов, определенных в базовом определении. Он также может определять ограничения количества элементов между типом и его дочерними типами. Это не специфические для языка ограничения (хотя они, кстати, могут быть), а ограничения, определяемые предметной областью.

Конфигурации диалекта

Они определяют реализации по умолчанию для представления данных на диалекте, таком как TypeScript или SQL. Кроме того, расширения субдиалекта позволят уточнить диалекты. Например, расширение диалекта SQL будет иметь поддиалекты, каждый из которых относится к определенной СУБД.

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

Базовая схема UTS «набрасывает» типы, используемые в приложении. Это полезно для разработчиков моделей данных, впервые моделирующих бизнес-домен. Еще предстоит обсудить несколько ключевых элементов: значения по умолчанию для диалектов, привязки диалектов к диалектам и пользовательские конфигурации.

Диалект по умолчанию

Чтобы сгенерировать типы для диалекта, мы должны знать, как сопоставить тип UTS с типами диалекта. В большинстве случаев разумные типы по умолчанию могут быть определены для любого диалекта. Этого будет недостаточно во всех случаях, поэтому должен быть способ переопределить эти сопоставления типов по умолчанию с помощью конфигурации или расширения.

Привязка диалекта к диалекту

Так что одно дело - сопоставить определения типов UTS с реальными языковыми типами, но во многих сценариях может потребоваться пересечение языковых границ, скажем, от «типов» JavaScript JSON к определениям данных SQL.

Схема привязки по умолчанию будет сгенерирована (или интерпретирована) UTS-совместимой структурой. Привязки могут быть изменены другим набором определений, как определено разработчиком моделей данных. Связывающий документ, в свою очередь, можно использовать для создания сопоставлений между типами в диалектах 1 и 2.

Существует значительная детализация и сложность, которые необходимо будет проработать в отношении реализации сопоставления данных между произвольными диалектами, но каждое сопоставление по идее будет аналогично объектно-реляционному сопоставлению или сопоставлению объектов JavaScript в / из содержимого POST REST API и определения результатов. .

Пользовательские конфигурации

Подобно сопоставлениям UTS-to-Language, привязки должны допускать настройку через файлы конфигурации. Эти файлы конфигурации будут изменять привязки по умолчанию (как предопределено UTS для различных языков). Конфигурация может изменять привязки типа к типу в различных областях: по языку, по модулю, по типу, поддиалекту языка (например, вариант SQL СУБД) или в других областях, которые подлежат определению.

Система универсальных типов действительно имеет смысл только в том случае, если сопоставления типов по умолчанию соответствуют правилу 80/20: 80% сопоставлений по умолчанию являются разумными, а 20% нуждаются в модификации. Для примитивных типов это часто бывает для конкретного языка; однако при привязке языка к языку могут возникнуть условия выхода за пределы диапазона.

Пример схемы работы

Из схемы UTS создаются как схема SQL, так и определения PODO TypeScript. Оба полностью соответствуют базовому определению UTS (плюс любые расширения домена). TypeScript и база данных должны взаимодействовать друг с другом, так что PODO TypeScript имеют механизм для извлечения и передачи данных из базы данных SQL. PODO TypeScript и таблицы SQL (по типам) называются связанными.

Преимущества

UTS предложит:

  1. быстрое создание прототипов с использованием различных реализаций диалектов, что полезно для демонстрации концепции или для информирования базовой реализации.
  2. предоставить абстрактную схему, которая инкапсулирует концепции предметной области, не увязая в излишне точных деталях реализации
  3. разумное межъязыковое сопоставление типов по умолчанию

Редакторы могут быть адаптированы к UTS для обеспечения связи (и поддержки) диалектов, поддиалектов и их значений конфигурации.

Дальнейшие подробности

Было бы разумно взглянуть на существующие языки определения типов и посмотреть, могут ли они быть приняты. Я упомянул GraphQL, но другие языки схем являются жизнеспособными базовыми кандидатами. Ключевая концепция - избежать жесткой привязки схемы к реализации (как это происходит с директивами GraphQL и аннотациями Java) и вместо этого сохранить эти отдельные задачи в отдельных файлах определений, помимо самой схемы.

Механизм UTS может генерировать код или интерпретировать схемы во время выполнения, или и то, и другое. Информация из схемы UTS может использоваться для наложения ограничений, которые не могут быть легко обработаны сгенерированным кодом, таких как ассоциативные ограничения данных.

Проблемы? Всегда есть проблемы.

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