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

18. Распространение

Это самая длинная глава в книге. Она оправдывает свою длину. Распространение в Archlang — тема, в которой язык сильнее всего отличается от всего, чем вы пользовались раньше, и тема, которая лучше всего вознаграждает за то, чтобы чётко уложить её в голове.

Коротко, заранее:

  • Механизмов распространения два, а не один.
  • Они работают в разное время и по разным осям.
  • Они используют намеренно различный словарь, чтобы вы могли отличать их при чтении.
  • Они компонуются — язык работает потому, что это так.

В этой главе вводятся оба механизма, показывается, как они выглядят в исходнике, и разбирается пример, где оба задействованы одновременно. Если вы вынесете из этой главы только одно: механизмов два.

Почему два

В архитектурных описаниях есть две естественные оси распространения:

  1. Ось типов. Подтип разделяет структуру со своим родительским типом. Конкретный тип микросервиса штампует общие значения по умолчанию на каждый экземпляр.
  2. Ось вложенности. Вложенный модуль разделяет контекст со своим родительским модулем. Сервисом владеет команда; компоненты внутри этого сервиса принадлежат той же команде.

Язык с одним механизмом распространения мог бы выразить одно из этого, но не другое чисто. У 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: Уточнение, переопределение и отбрасывание → — единый словарь для редактирования того, что предоставляют типы и родители.