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

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

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

Пользовательский вид модуля

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

Наивный путь: копировать-вставлять эти три строки в каждый внутренний сервис. Лучший путь: определить вид.

type module internal_service {
required cascade team
labels {
security.zone: Internal
}
}

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

internal_service Inventory {
team: Commerce
"Tracks stock counts in real time."
command CheckAvailability
command Reserve
}

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

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

type service internal_service {
required cascade team
labels { security.zone: Internal }
}

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

Форма объявления типа

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

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

Пользовательский вид фасета

Фасет для «HTTP-ресурса»:

type facet resource {
"A facet 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"
command Post
query Get
resource Items {
base: "/items" // appends to /orders → /orders/items
query 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Обязательный пропуск, который после заполнения каскадирует
labels { x: y }Значения меток по умолчанию
required labels.xОбязательный пропуск метки
command X { ... }Предзаполненный интерфейс — экземпляр наследует
required command XОбязательный пропуск интерфейса — экземпляр должен уточнить
component Y { ... }Предзаполненный подмодуль
required component YОбязательный пропуск подмодуля

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

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

Где живут типы

Типы живут в .arch-файлах. По соглашению: храните их в файле с именем kinds.arch в корне пакета для наглядности.

acme.shop/
├── package.archspace
├── kinds.arch # type module internal_service { ... }
├── payments.arch # internal_service Payments { ... }
└── orders.arch # internal_service Orders { ... }

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

export type module internal_service {
required cascade team
labels { security.zone: Internal }
}

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

Что не может быть родительским видом

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

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

Проработанный срез метамодели

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

kinds.arch
// A service that operates in PCI scope.
export type service pci_service {
required cascade team
labels {
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 {
team: Payments
ext.runbook_url: "https://wiki/auth-runbook"
command Authorize
}
card_processor StripeIntegration {
team: Payments
ext.runbook_url: "https://wiki/stripe-runbook"
ext.processor_vendor: "Stripe"
ext.api_docs_url: "https://stripe.com/docs"
command Charge
webhook PaymentSucceeded
webhook PaymentFailed
}

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

Резюме

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

Что дальше

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