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: Ключевые слова перечисляет полный набор.
Проработанный срез метамодели
Заголовок раздела «Проработанный срез метамодели»Вот небольшой набор связанных типов, определяющих словарь домена платежей:
// 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: Обязательные пропуски → — механизм обязательного решения, который делает типы чем-то большим, чем просто синтаксический сахар.