18. Распространение
Это самая длинная глава в книге. Она оправдывает свою длину. Распространение в Archlang — тема, в которой язык сильнее всего отличается от всего, чем вы пользовались раньше, и тема, которая лучше всего вознаграждает за то, чтобы чётко уложить её в голове.
Коротко, заранее:
- Механизмов распространения два, а не один.
- Они работают в разное время и по разным осям.
- Они используют намеренно различный словарь, чтобы вы могли отличать их при чтении.
- Они компонуются — язык работает потому, что это так.
В этой главе вводятся оба механизма, показывается, как они выглядят в исходнике, и разбирается пример, где оба задействованы одновременно. Если вы вынесете из этой главы только одно: механизмов два.
Почему два
В архитектурных описаниях есть две естественные оси распространения:
- Ось типов. Подтип разделяет структуру со своим родительским типом. Конкретный тип микросервиса штампует общие значения по умолчанию на каждый экземпляр.
- Ось вложенности. Вложенный модуль разделяет контекст со своим родительским модулем. Сервисом владеет команда; компоненты внутри этого сервиса принадлежат той же команде.
Язык с одним механизмом распространения мог бы выразить одно из этого, но не другое чисто. У Archlang оба.
| Ось | Механизм | Когда работает | Действует на |
|---|---|---|---|
| Тип → подтип/экземпляр | Штамповка шаблона (механизм A) | Время разбора | Цепочка наследования типов |
| Родительский модуль → дети/фасеты/интерфейсы | Структурный каскад (механизм B) | Время разрешения | Дерево вложенности |
Словарь намеренно разделён:
- Для механизма A мы говорим наследовать, штамповать, авто-распространять.
- Для механизма B мы говорим каскадировать, течь, распространяться через, получать от предка.
«Наследовать» зарезервировано для смысла из системы типов. Когда описание в исходнике использует «наследовать», оно всегда имеет в виду механизм A.
Механизм A: штамповка шаблона
Тело типа действует как шаблон формы. Когда парсер встречает экземпляр такого типа, содержимое шаблона — значения по умолчанию, пропуски, под-объявления, метки, модификаторы распространения — штампуется на экземпляр так, словно записано там буквально.
type module service { component metrics { command Emit } // pre-filled sub-declaration required cascade team // mandatory blank field}
service Payments { team: Payments // After parsing, Payments effectively has: // team: Payments // component metrics { command Emit } // (cascade modifier on team)}Компонент metrics появляется у каждого экземпляра микросервиса. Модификатор cascade на team тоже отправляется вместе с экземпляром — он будет важен на этапе разрешения, но штамповка его туда поместила.
Штамповка каскадирует через цепочку типов. Подтип paymentsService от service штампует своё содержимое и содержимое service на каждый экземпляр paymentsService:
type module service { component metrics { command Emit }}
type service paymentsService { component metrics_pci_extras { command EmitPCI }}
paymentsService Authorize { // After parsing, Authorize effectively has: // component metrics { command Emit } (from service) // component metrics_pci_extras { command EmitPCI } (from paymentsService)}Словарь: штамповка типа, наследование шаблона.
Когда: во время разбора, до любого разрешения имён.
Где: вдоль цепочки наследования типов.
Модификаторы: required, значения по умолчанию, под-объявления.
Правки: override, drop, уточнение (следующая глава).
Механизм B: структурный каскад
После разбора модель представляет собой дерево вложенных модулей. Некоторые поля помечены cascade или append на уровне типа; метки всегда каскадируют неявно. Когда что-то ищет своё значение team, резолвер обходит вверх по структурному дереву, чтобы найти ближайшее установленное значение.
service Payments { team: Payments
component MetricsExporter { // No 'team' set here. // Resolution time: MetricsExporter.team — walk up. // MetricsExporter.team unset → check parent (Payments) → "Payments" // Effective MetricsExporter.team is "Payments". }}Значение течёт от родителя к потомку через вложенность, а не через отношения типов. MetricsExporter — это дочерний модуль Payments. Каскад идёт «вниз по дереву вложенности».
Словарь: каскадировать, течь, распространяться.
Когда: во время разрешения значений (когда что-то спрашивает значение).
Где: вдоль структурного дерева вложенности.
Модификаторы: cascade (переопределение при обходе), append (композирование при обходе), без модификатора (локальное — не течёт).
Правки: установить свежее значение на нужном уровне; drop, чтобы разорвать цепочку.
Cascade против append против локального
Поля подключаются к структурному распространению через модификатор в объявлении типа. Три поведения:
| Модификатор | Поведение |
|---|---|
| (нет) — локальное | Поле описывает только ту сущность, на которой объявлено. Не течёт к потомкам. |
cascade | Значение течёт к потомкам; потомки переопределяют, устанавливая своё. |
append | Значение течёт к потомкам и композируется согласно типу поля — пути склеиваются, списки дописываются, объекты сливаются. |
type module service { required cascade team // cascade: override-on-walk append tags // append: compose-on-walk version // local: stays where it's set}Каскад в деле — переопределение при обходе:
service Orders { team: Commerce
component Outbox { // No 'team' set. Walks up: // Outbox.team → "Commerce" (cascaded from Orders) }
component Worker { team: WorkerOps // overrides the cascade locally // Worker.team → "WorkerOps" }}Append в деле — композирование при обходе:
service Orders { team: Commerce tags: ["pii", "audit"]
component Outbox { // No 'tags' set. Walks up; append composes: // Outbox.tags → ["pii", "audit"] (inherited as-is) }
component Worker { tags: ["batch"] // appends to inherited list // Worker.tags → ["pii", "audit", "batch"] }}Локальное остаётся на месте:
service Orders { team: Commerce version: v2
component Worker { // No 'version' set. version is local — does NOT walk up. // Worker.version → undefined }}Поведение каскада зафиксировано у типа, который вводит поле. Подтипы и экземпляры не могут изменить cascade-поле на local или наоборот. Режим неотделим от смысла поля.
Метки всегда каскадируют-переопределяют
Метки (то, что внутри labels { }) всегда каскадируют с семантикой переопределения. Вы не пишете cascade на метке; каскад заложен в само понятие метки. Причина: метки существуют, чтобы по ним проецировать (проекции группируют по ним, фокусируются на них) — метка, которая не течёт, была бы бесполезна для проекции.
service Orders { labels { domain: Orders }
component Worker { // Worker.labels.domain → "Orders" (cascaded)
labels { domain: WorkerDomain } // Now Worker.labels.domain → "WorkerDomain" (override) }}Если вам нужно накопление вместо переопределения (список меток, растущий от родителя к ребёнку), используйте поле с append, а не метку. Метки не могут композироваться; поля могут.
Оба механизма вместе
В большинстве нетривиальных моделей задействованы оба механизма одновременно.
// (A) Type provides defaults and a cascade modifier.type module service { required cascade team component metrics { command Emit }}
// (A) Subtype stamps a fulfilled team and inherits everything else.type service paymentsService { team: "Payments" // fulfills the blank for descendants}
// At parse time, mechanism A stamps onto Payments:// - team: "Payments" (from paymentsService)// - component metrics { command Emit } (from service)// - cascade behavior on team (from service)
paymentsService Payments { "Core payment processing"
component MetricsExporter { command Forward }}
// At resolution time, mechanism B walks the structural tree:// Payments.team → "Payments" (set on Payments itself by stamping)// Payments.MetricsExporter.team → "Payments" (cascaded from Payments)// Payments.metrics.team → "Payments" (cascaded from Payments,// via stamping that put metrics here)// Payments.metrics.Emit → no team field at the interface level,// resolves to "Payments" via cascadeЭкземпляр читается естественно — нет повторяющегося team: на каждом вложенном элементе — потому что механизм A посадил модификатор каскада и заполненное значение, а механизм B обходит дерево во время поиска.
Что побеждает, когда применимы оба
Есть одно правило разрешения конфликтов, которое стоит запомнить:
Вложенное значение побеждает штампованное значение по умолчанию.
Когда значение достижимо и через штамповку типа (значение по умолчанию, предоставленное типом), и через структурный каскад (его установил вложенный предок), побеждает вложенное значение. Вложенность более локальна и более явна; штампованное значение по умолчанию — более широкий и слабый источник.
type module service { cascade widget: arch-service // type default}
service Container { widget: arch-special // nested ancestor's value
component Inside { // Inside.widget — both sources apply: // stamped default: arch-service // cascade from Container: arch-special // Nested wins. Inside.widget → "arch-special" }}Почему два компонуются лучше, чем один
Представьте язык только с механизмом A (штамповка шаблона). Чтобы выразить «каждый вложенный компонент наследует команду родителя», тип должен был бы знать про каждую возможную глубину вложенности и штамповать team на каждом уровне. Это невозможно, потому что тип не знает, как экземпляры будут вкладываться.
Представьте язык только с механизмом B (структурный каскад). Чтобы выразить «у каждого микросервиса по умолчанию есть компонент metrics», некуда положить это значение по умолчанию — поля каскадируют, а под-объявления — нет.
Каждый механизм делает то, чего не может другой. Вместе они покрывают пространство:
- A говорит, какие поля и под-объявления существуют на экземплярах.
- B говорит, как значения текут, когда экземпляры оказались в дереве вложенности.
Нужны оба. Язык даёт оба с различным словарём, чтобы вы могли понять, что именно происходит, когда читаете исходник.
drop абсолютен на каскадных цепочках
Когда вы делаете drop поля или метки в теле, каскадная цепочка разрывается в точке отбрасывания. Потомки не возобновляют чтение из более глубокого предка.
service Orders { team: Commerce // cascades
component Special { drop team // breaks the chain // Special.team → undefined // (NOT "walks up past the drop to find the next ancestor")
component Inner { // Inner.team → undefined (chain still broken) } }}Это сделано сознательно. drop — это утверждение модели «здесь намеренно нет значения». Если бы каскад просачивался мимо, это молчаливо отменяло бы намерение автора.
Резюме
- Есть два механизма распространения: штамповка шаблона (A) и структурный каскад (B).
- Механизм A работает во время разбора, вдоль цепочки наследования типов. Словарь: наследовать, штамповать.
- Механизм B работает во время разрешения, вдоль структурного дерева вложенности. Словарь: каскадировать, течь.
cascade(переопределение при обходе),append(композирование при обходе) и без модификатора (локальное) — три режима каскада на уровне поля.- Метки всегда каскадируют с семантикой переопределения. Для накопления используйте поле с
append. - Два механизма компонуются; правило разрешения конфликтов — вложенное побеждает штампованное.
dropабсолютен — он разрывает каскадную цепочку в этой области.
Что дальше
Глава 19: Уточнение, переопределение и отбрасывание → — единый словарь для редактирования того, что предоставляют типы и родители.