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

9. Поля и аспекты

Файл .arch в основном структурный: объявите модуль, объявите интерфейс, объявите процесс. Неструктурная часть — владельцы, версии, URL, оси классификации — живёт в двух связанных, но различных конструкциях: полях и аспектах. Вместе с описаниями (Глава 10) это три способа аннотировать любой элемент. Чёткое разделение:

  • Аспекты = неявные межплоскостные связи. Аспект связывает вещь с другой плоскостью архитектуры — плоскостью данных, плоскостью хостинга, плоскостью обмена сообщениями — а не с соседом на её собственной плоскости.
  • Поля = структурированные данные, которые вы явно не хотите взаимосвязывать. Версия, статус, URL репозитория, обязательное свойство, управляющий элемент виджета. Они остаются локальными для элемента.
  • Описания = неструктурированная проза.

Эта глава — про первые две. Короткая версия: если это свойство вещи, используйте поле; если это связь с другой плоскостью — что-то, по чему вы когда-либо сделали бы group by или сфокусировали бы focus проекции, — используйте аспект.

Поля живут прямо в теле, без обрамляющего блока:

module Orders {
status: active
version: v2
repo.url: "https://github.com/acme/orders"
ext.cmdb.ci: "CI28304858"
base: "/orders"
"Order management service"
}

Правило. Разделяйте токены одним пробелом. Не добавляйте лишние пробелы, чтобы выровнять значения в столбцы, — выравнивание выглядит аккуратно, но гниёт, потому что каждое переименование заново ломает столбцы и порождает шумные диффы, состоящие из одного только выравнивания.

Каждое поле — это key: value, где:

  • Ключ — идентификатор или путь через точку (repo.url, ext.cmdb.ci). Точечные ключи эквивалентны вложенной объектной структуре; инструменты обрабатывают их единообразно.
  • Значение — один из четырёх скалярных типов:
    • идентификаторCommerce, v2
    • строка в кавычках"any text", многострочные разрешены
    • число42, 3.14
    • булевоtrue, false

Это весь язык значений. Никаких вложенных объектов, никакой системы типов на каждое поле, никаких дженериков, никаких пользовательских форм значений. Инструменты могут интерпретировать значения по соглашению (URL становятся кликабельными, точечные идентификаторы — копируемыми), но парсер обращается с ними со всеми как со скалярами.

Смена мышления. Если вы пришли из конфигурационных языков вроде HCL, JSON или YAML, отсутствие вложенных объектов может ощущаться как ограничение. Это намеренно. Архитектурные описания не нуждаются в выразительности конфигурации; им нужен плоский слой ключ-значение, по которому тривиально делать запросы и группировки. Форма с точкой в ключе (repo.url) покрывает то, для чего использовались бы вложенные объекты.

Голая строка — это поле с зарезервированным смыслом: это описание:

module Payments {
"Core payment processing service for the platform."
}

В теле может быть несколько голых строк-описаний; они склеиваются через \n в порядке объявления. Это позволяет писать многоабзацные описания без одной огромной многострочной строки. Описания рендерятся как markdown плюс два расширения — см. Главу 10.

Аспекты идут внутрь блока aspect { }:

module Payments {
aspect {
team: "Payments"
domain: "Payments"
security.zone: "PCI"
network.segment: "Internal"
criticality: "High"
}
}

Каждый key: value внутри aspect { } — это один аспект. Ключи могут быть с точкой (security.zone, network.segment); значения используют те же скалярные типы, что и поля. Лексическая форма значения аспекта несёт смысл — см. Классификация и членство ниже.

Аспекты:

  • Каскадируются. Аспект, объявленный на родительском модуле, виден на каждом вложенном модуле, поверхности и интерфейсе — если они не объявят тот же аспект сами. Именно так aspect domain: "Payments" на модуле верхнего уровня достигает каждой операции внутри без повторов. См. Главу 18.
  • Управляют проекциями. show @@domain:"Payments" работает, потому что domain — ключ аспекта; group by @@team группирует по аспекту team. Сигил @@ называет ось аспекта в любом геттере или селекторе.
  • Управляют проверками. Валидаторы читают аспекты: каждый PCI-сервис должен иметь записанный security.contact, ни один сервис в network.segment: DMZ не может вызывать сервис в network.segment: Internal.

Одиночный аспект не нуждается в блоке. Однострочная форма эквивалентна и для одного значения читается лучше:

module Cart {
aspect domain: "Payments"
}

Ключевое слово не ставит точку после себя (aspect domain, а не aspect.domain) — оно вводит исключительный конструкт, а не пространство имён поля.

Выбирайте по числу значений, которые пишете:

  • Одно значение → без блока. aspect domain: "Payments".
  • Пара (≈2) → любой вариант. Блок aspect { } или две отдельные строки; на ваше усмотрение. Короткий встроенный блок тоже годится — aspect { domain: "Payments"; criticality: "High" }, — но ограничивайте встроенные блоки 2 парами, максимум 3, а затем переносите на несколько строк.
  • Много (≈5+) → предпочтите блок. Форма без блока не запрещена, но блок aspect { } при таком размере читается лучше.

Ни одна из нотаций никогда не обязательна — это предпочтения, а не правила. (Записи внутри блока разделяются ; или переносом строки — перенос строки идиоматичен в многострочном блоке; запятая между записями блока — ошибка разбора.)

Лексическая форма значения аспекта решает, что он означает. Это единственное место, где наличие или отсутствие кавычек меняет семантику:

aspect {
domain: "Payments" // классификация — общая групповая идентичность
security.zone: "PCI" // классификация
netzone: Dmz // членство — Dmz становится местом на плоскости `netzone`
}
  • Строковое значение (key: "x") → классификация. Чистая идентичность, заданная парой (key, value). Каждое вхождение одного и того же (key, "x") обозначает одно и то же неявное место, поэтому классификации переименовываются как единое целое и образуют оверлей участник → группа. Идентичность задаётся по каждому ключу отдельно: netzone: "dmz" и tier: "dmz" — разные аспекты, которые просто разделяют значение. Списочное значение — это множественная классификация/членство: tag: ["edge", "public"] присоединяет модуль к обеим группам.
  • Голое значение (key: x) → членство в модуле x. x обязан разрешиться в реальный модуль — так он становится местом на плоскости key, а участник получает присутствие на этой плоскости. Голые ссылки строгие — значение, которое не разрешается в модуль, — ошибка (ASPECT_BAD_REF); быстрое исправление в редакторе берёт его в кавычки, превращая в классификацию. Кавычки — лазейка, когда вы имеете в виду тег, а не модуль.

Сомневаетесь — берите в кавычки. Классификация в кавычках всегда валидна; голая ссылка обязана разрешиться. Большинство осей классификации (domain, security.zone, criticality) — это классификации, поэтому большинство значений аспектов — строки.

В проекции сигил @@ отмечает ось аспекта, и действует то же правило кавычек: show @@domain:"Payments" совпадает с классификацией в кавычках, а group by @@network.segment читает ось. См. Главу 8.

Ментальная модель за аспектами: ключ — это плоскость существования (security.zone, broker, host), а значение — это место на ней (DMZ, kafka-broker, тот самый экземпляр Postgres). Два элемента, разделяющие ключ и значение, живут в одном месте на одной плоскости — слабая связь «эти принадлежат друг другу», а не жёсткое ребро вызова. Вот почему вложенность и аспекты делают разную работу: вложенность — это глубина домена (B — часть A), тогда как аспект — это связь с другой плоскостью (B развёрнут на / маршрутизируется через X).

Именно это позволяет моделировать бизнес-уровень без транспортной обвязки. Не рисуйте ServiceA → Kafka broker → ServiceB — это хоронит реальную зависимость под инфраструктурой и направляет каждый сервис на один и тот же узел брокера. Вместо этого рисуйте логический вызов (ServiceA вызывает интерфейс kafka на ServiceB) и вешайте брокер на аспект:

module ServiceB {
kafka orderEvents {
aspect broker: KafkaBroker
}
}

Брокер становится местом на собственной плоскости через членство — настоящим, отрисовываемым модулем, выводимым как переключаемый слой, когда вы хотите увидеть, какие связи через какой брокер идут, — а не путевой точкой в графе вызовов. Плоскость хостинга выстраивается в цепочку так же: вложенная database несёт аспект dbms к СУБД, на которой работает, а та несёт аспект host к своему серверу. Каждый переход — это другая плоскость, поэтому каждый — это аспект, а никогда не вложенность, которая ошибочно заявила бы «часть домена».

Практическое правило снова, с примерами:

Используйте аспект, когдаИспользуйте поле, когда
По нему когда-либо захочется group byЗначение — единый факт, уникальный для этой вещи
Фильтруете проекцию по нему (show @@…)Значение — URL, идентификатор, версия или счётчик
Это ось классификацииЭто идентификатор или указатель
Примеры: domain, security.zone, team, criticalityПримеры: repo.url, version, ext.cmdb.ci, base

team стоит на стороне аспектов — стандартная библиотека определяет его как aspect team, каскадирующую ось классификации. По правилу выше ему там и место: вы делаете group by @@team и нарезаете по нему проекции — а именно это и делает вещь аспектом, а не полем. Выбор был сделан один раз в стандартной библиотеке, так что каждый проект разделяет одно и то же соглашение.

Обратите внимание на одну практическую деталь: аспекты всегда ведут себя в распространении как cascade-override. У вас не может быть аспекта «append». Если нужна аккумуляция (список тегов, растущий от родителя к ребёнку), используйте поле с модификатором append — см. Главу 18.

Значения-строки в кавычках могут занимать несколько строк. Лексер автоматически снимает отступ:

module Payments {
"
Core payment processing.
Owns authorization, capture, and refund.
Publishes events to the order pipeline.
"
}

Ведущая строка только из пробелов отбрасывается; минимальный отступ, общий для остальных строк, удаляется. Приведённое выше даёт значение "Core payment processing.\nOwns authorization, capture, and refund.\nPublishes events to the order pipeline.".

Единственная распознаваемая последовательность экранирования — \".

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

module Cart {
widget: """
<div class="card" data-name="{{name}}">
<span class="label">{{team}}</span>
</div>
"""
}

Строки в тройных кавычках — сырые. Они не поддерживают интерполяцию в стиле ${expr} (так что будущие возможности шаблонных литералов можно будет добавить позже без конфликтов).

В ранних версиях ArchLang был блок props { } для хранения «дополнительных метаданных». Его больше нет. Эта роль полностью покрывается полями с точечными ключами: ext.cmdb.ci: "...", repo.url: "...". Если вы читаете старые файлы с props { }, запустите форматтер — он их вынесет.

  • Три способа аннотировать: аспекты — межплоскостные связи, поля — структурированные данные, которые вы не хотите взаимосвязывать, описания — неструктурированная проза.
  • Поля описывают, чем вещь является, и живут прямо в теле, без обрамляющего блока.
  • Аспекты связывают вещь с местом на другой плоскости — как классификация или как членство; они живут в aspect { } или как строка aspect key:.
  • Форматируйте по числу значений: одно значение → однострочная форма; несколько → блок; ограничивайте встроенные блоки 2–3 парами. Одиночные пробелы, никогда не выравнивание в столбцы.
  • Значения — скаляры: идентификатор, строка в кавычках, число, булево. Никаких вложенных объектов.
  • Описания голыми строками зарезервированы; несколько голых строк склеиваются.
  • Аспекты управляют проекциями и каскадируются через вложенность; если нужна аккумуляция, используйте поле с append.
  • Выбор «поле или аспект?» обычно сводится к: захотите ли вы group by или focus по этому?

Глава 10: Описания → — подмножество markdown и синтаксис перекрёстных ссылок внутри строк-описаний.