Перейти к содержимому

16. Определение типов

Вы потребляли типы из стандартной библиотеки с главы 4. Теперь вы напишете свои. Эта глава знакомит с тремя: пользовательский тип модуля, пользовательский тип поверхности и пользовательский тип интерфейса. Каждый показывает свою форму тела типа.

Правило. Имена типов — в lower_snake_case: internal_service, pci_service, relational_db. (Модули — в UpperCamelCase, интерфейсы — в lowerCamelCase; у типов свой регистр, чтобы читатель с первого взгляда мог отличить тип от экземпляра.)

Прежде чем писать что-либо из этого, вспомните главу 15: пользовательский тип должен окупаться. Каждый пример ниже добавляет реальное требование, аспект, значение по умолчанию или виджет — но никогда безделушку-обёртку. Если ваш кандидат в типы не добавит ничего из этого, используйте вместо него простой модуль.

Допустим, у вашей команды есть понятие внутреннего сервиса — микросервиса, доступного только изнутри вашего VPC, обязанного объявлять версию, в которой он запущен, и помеченного security.zone: Internal. Каждый экземпляр — это микросервис с этими тремя вещами, запечёнными внутрь.

Наивный путь: копировать-вставлять эти три строки в каждый внутренний сервис. Лучший путь: определить тип. Этот окупается — он несёт обязательное поле и аспект по умолчанию, так что это не безделушка-обёртка.

type module internal_service {
required cascade version
aspect {
security.zone: "Internal"
}
}

Теперь экземпляр:

internal_service Inventory {
version: "1.4"
"Tracks stock counts in real time."
rest_create checkAvailability
rest_create reserve
}

Inventory автоматически получает аспект security.zone: "Internal". Он всё ещё обязан заполнить version, потому что тип пометил его required — это тема следующей главы.

Обратите внимание, что internal_service объявлен с module в качестве родительского типа. Это делает его обобщённым подтипом модуля — вы также могли бы сделать подтипом service, если бы хотели визуальную обработку микросервиса плюс ограничения internal_service:

type service internal_service {
required cascade version
aspect { security.zone: "Internal" }
}

Теперь экземпляры internal_service отрисовываются виджетом service по умолчанию (унаследованным от типа service из стандартной библиотеки), вдобавок имея аспекты и обязательное version.

type <stable_id?> <parent_type> <name> { <body> }
  • <stable_id?> — необязательный слот #xyz. Форматтер выдаёт его при сохранении.
  • <parent_type> — тип, который этот расширяет. Любой зарегистрированный тип. module, surface, interface — три базовых типа; всё остальное — пользовательский подтип где-то выше по цепочке.
  • <name> — имя, которое будут использовать ваши экземпляры, в lower_snake_case.
  • <body> — значения по умолчанию, пропуски, под-объявления и модификаторы.

Ключевого слова extends здесь нет. Родитель кодируется позицией. type service payments_service { ... } означает «payments_service расширяет service».

Поверхность для «HTTP-ресурса»:

type surface resource {
"A surface representing an HTTP resource. Path composes via append."
append base
}

append base говорит: экземпляры resource несут поле base, которое композирует со значениями потомков (родительский /orders и дочерний /items разрешаются в /orders/items). Модификатор append — тема главы 18; пока примите его как «способ, которым составляющие пути складываются».

Используется так:

service Orders {
resource OrdersResource {
base: "/orders"
rest_create post
rest_read get
resource Items {
base: "/items" // appends to /orders → /orders/items
rest_read list
}
}
}

Для типа интерфейса webhook, у которого по умолчанию поле protocol: webhook:

type interface webhook {
protocol: webhook
"An HTTP webhook callback delivered by an external system."
}

Используется так:

service OrderWebhooks {
webhook orderCreated
webhook orderCancelled
}

Каждый экземпляр webhook наследует protocol: webhook. Экземпляр может переопределить или отбросить его.

Каждая форма, которую мы уже встречали как элемент тела, может появиться в теле типа, с добавлением маркеров required и модификаторов cascade / append на полях:

В теле типаЗначение
field: valueЗначение по умолчанию для экземпляров
required fieldОбязательный пропуск — экземпляр должен заполнить
cascade field: valueЗначение по умолчанию, каскадирующее к потомкам
append fieldПоле, композирующее со значениями потомков
required cascade fieldОбязательный пропуск, который после заполнения каскадирует
aspect { x: y }Значения аспектов по умолчанию
required aspect xОбязательный пропуск аспекта
rest_create X { ... }Предзаполненный интерфейс — экземпляр наследует
required rest_create XОбязательный пропуск интерфейса — экземпляр должен уточнить
component Y { ... }Предзаполненный подмодуль
required component YОбязательный пропуск подмодуля

Сочетания required, cascade и append — это язык для проектирования вашей метамодели.

Ошибка, которой стоит избегать. required component logs { rest_create Send } — противоречие. required означает «нет значения»; фигурный блок означает «вот значение». Валидатор это отвергает. Либо помечайте required (пропуск, который нужно заполнить), либо предоставляйте содержимое (без required).

Типы живут в .arch-файлах, и правило здесь то же, что управляет каждым файлом в рабочем пространстве: организуйте по домену, а не по типу элемента. Держите тип рядом с его экземплярами — в том же доменном файле, что и модули, которые его используют, или в доменно-ограниченном файле рядом с ними.

Не сметайте все типы в голый types.arch; файл, названный по синтаксической категории (types.arch, processes.arch), — это антипаттерн. Вместо этого называйте файл по домену или словарю, который он несёт:

acme.shop/
├── package.archspace
├── payments.arch # payment modules + their payment types
├── orders.arch # order modules + their order types
└── business-primitives.arch # shared business types used across domains

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

Чтобы сделать тип видимым для других пакетов, пометьте его export:

export type module internal_service {
required cascade version
aspect { security.zone: "Internal" }
}

Без export тип внутренний для своего пакета. Импортёры пользуются механизмом use из главы 12.

Два ограничения:

  • process и view не могут быть родительскими типами. Их тела не являются штампуемыми шаблонами (это последовательности и проекции соответственно), поэтому их подтипизация пока не имеет полезной семантики. Зарезервировано для будущих итераций.
  • Зарезервированные ключевые слова не могут быть именами типов. Нельзя написать type module process { ... }, потому что process — ключевое слово. Приложение B: Ключевые слова перечисляет полный набор.

Вот небольшой набор связанных типов, определяющих словарь домена платежей:

types.arch
// A service that operates in PCI scope.
export type service pci_service {
required cascade version
aspect {
security.zone: "PCI"
}
required ext.runbook_url
}
// A service that processes external card transactions.
export type pci_service card_processor {
required ext.processor_vendor
required ext.api_docs_url
"A card processor — talks to an external vendor and is in PCI scope."
}
// An interface for a callback delivered by an external system.
export type interface webhook {
protocol: webhook
"An HTTP webhook callback."
}

И затем в экземплярах:

pci_service Authorize {
version: "2.1"
ext.runbook_url: "https://wiki/auth-runbook"
rest_create authorize
}
card_processor StripeIntegration {
version: "3.0"
ext.runbook_url: "https://wiki/stripe-runbook"
ext.processor_vendor: "Stripe"
ext.api_docs_url: "https://stripe.com/docs"
rest_create charge
webhook paymentSucceeded
webhook paymentFailed
}

Метамодель кодирует доменное знание: у каждого PCI-сервиса есть URL с инструкцией по эксплуатации; каждый обработчик карт называет своего поставщика и документацию API. Валидатор обеспечивает выполнение этих требований во время разбора. Словарь (pci_service, card_processor, webhook) естественно читается в исходнике.

Набор типов — это метамодель. Библиотека пользовательских типов — это больше, чем удобный набор. Налагая требования и руководящие принципы, она становится метамоделью — дисциплиной, которая управляет тем, как думает команда. Две линзы на один и тот же механизм: линза приятных дополнений (удобные виджеты и модели, как actor и group из arch.extras) и линза метамодели (нотация, формирующая декомпозицию, как C4, выраженный библиотекой ArchLang). Когда вы упаковываете эти платёжные типы, чтобы другие могли их use, выбирайте по тому, что они делают с вашим мышлением, а не только по компонентам, которые они выдают.

  • Тип расширяет родительский тип позиционно: type <parent> <name>. Без ключевого слова extends.
  • Тела типов содержат значения по умолчанию, обязательные пропуски, предзаполненные под-объявления и модификаторы распространения.
  • Имена типов — в lower_snake_case; пользовательский тип должен окупаться (требование, аспект, значение по умолчанию или виджет) — но никогда не быть безделушкой-обёрткой.
  • module, surface, interface — три базовых типа; всё остальное — подтип.
  • process и view (пока) не могут быть родительскими типами.
  • Экспортируйте типы через export type ..., чтобы сделать их видимыми для импортирующих пакетов.
  • Держите типы доменно близко к их экземплярам; связанный набор типов — это метамодель.

Глава 17: Обязательные пропуски → — механизм обязательного решения, который делает типы чем-то большим, чем просто синтаксический сахар.