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

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

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

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

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

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

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

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

Язык с одним механизмом распространения мог бы выразить одно из этого, но не другое чисто. У ArchLang оба.

ОсьМеханизмКогда работаетДействует на
Тип → подтип/экземплярШтамповка шаблона (механизм A)Время разбораЦепочка наследования типов
Родительский модуль → дети/поверхности/интерфейсыСтруктурный каскад (механизм B)Время разрешенияДерево вложенности

Словарь намеренно разделён:

  • Для механизма A мы говорим наследовать, штамповать, авто-распространять.
  • Для механизма B мы говорим каскадировать, течь, распространяться через, получать от предка.

«Наследовать» зарезервировано для смысла из системы типов. Когда описание в исходнике использует «наследовать», оно всегда имеет в виду механизм A.

Тело типа действует как шаблон формы. Когда парсер встречает экземпляр такого типа, содержимое шаблона — значения по умолчанию, пропуски, под-объявления, аспекты, модификаторы распространения — штампуется на экземпляр так, словно записано там буквально.

type module service {
component metrics { rest_create emit } // pre-filled sub-declaration
required cascade version // mandatory blank field
}
service Payments {
version: "2.1"
// After parsing, Payments effectively has:
// version: "2.1"
// component metrics { rest_create emit }
// (cascade modifier on version)
}

Компонент metrics появляется у каждого экземпляра микросервиса. Модификатор cascade на version тоже отправляется вместе с экземпляром — он будет важен на этапе разрешения, но штамповка его туда поместила.

Штамповка каскадирует через цепочку типов. Подтип payments_service от service штампует своё содержимое и содержимое service на каждый экземпляр payments_service:

type module service {
component metrics { rest_create emit }
}
type service payments_service {
component metrics_pci_extras { rest_create emitPCI }
}
payments_service Authorize {
// After parsing, Authorize effectively has:
// component metrics { rest_create emit } (from service)
// component metrics_pci_extras { rest_create emitPCI } (from payments_service)
}

Словарь: штамповка типа, наследование шаблона. Когда: во время разбора, до любого разрешения имён. Где: вдоль цепочки наследования типов. Модификаторы: required, значения по умолчанию, под-объявления. Правки: override, drop, уточнение (следующая глава).

После разбора модель представляет собой дерево вложенных модулей. Некоторые поля помечены cascade или append на уровне типа; аспекты всегда каскадируют неявно. Когда что-то ищет своё значение version, резолвер обходит вверх по структурному дереву, чтобы найти ближайшее установленное значение.

service Payments {
version: "2.1"
component MetricsExporter {
// No 'version' set here.
// Resolution time: MetricsExporter.version — walk up.
// MetricsExporter.version unset → check parent (Payments) → "2.1"
// Effective MetricsExporter.version is "2.1".
}
}

Значение течёт от родителя к потомку через вложенность, а не через отношения типов. MetricsExporter — это дочерний модуль Payments. Каскад идёт «вниз по дереву вложенности».

Словарь: каскадировать, течь, распространяться. Когда: во время разрешения значений (когда что-то спрашивает значение). Где: вдоль структурного дерева вложенности. Модификаторы: cascade (переопределение при обходе), append (композирование при обходе), без модификатора (локальное — не течёт). Правки: установить свежее значение на нужном уровне; drop, чтобы разорвать цепочку.

Поля подключаются к структурному распространению через модификатор в объявлении типа. Три поведения:

МодификаторПоведение
(нет)локальноеПоле описывает только ту сущность, на которой объявлено. Не течёт к потомкам.
cascadeЗначение течёт к потомкам; потомки переопределяют, устанавливая своё.
appendЗначение течёт к потомкам и композируется согласно типу поля — пути склеиваются, списки дописываются, объекты сливаются.
type module service {
required cascade version // cascade: override-on-walk
append tags // append: compose-on-walk
status // local: stays where it's set
}

Каскад в деле — переопределение при обходе:

service Orders {
version: "2.4"
component Outbox {
// No 'version' set. Walks up:
// Outbox.version → "2.4" (cascaded from Orders)
}
component Worker {
version: "3.0" // overrides the cascade locally
// Worker.version → "3.0"
}
}

Append в деле — композирование при обходе:

service Orders {
version: "2.4"
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 {
version: "2.4"
status: healthy
component Worker {
// No 'status' set. status is local — does NOT walk up.
// Worker.status → undefined
}
}

Поведение каскада зафиксировано у типа, который вводит поле. Подтипы и экземпляры не могут изменить cascade-поле на local или наоборот. Режим неотделим от смысла поля.

Когда у поля есть структурированные под-поля, которые концептуально принадлежат друг другу (вспомните widget + widget.icon + widget.color + …), объявить их все по отдельности как cascade можно, но переопределения вниз по цепочке становятся шумными: если подтип меняет корневой тег, каждый лист придётся отбрасывать через drop вручную.

cascade * объявляет поле как корень каскадной группы. Под-поля под этим путём участвуют в той же группе, и группа в целом реагирует на изменения корня.

type module service {
cascade * widget: arch-module {
icon: service
color: info
subheader: @@team
}
}

Тело { … } — это тело группы. Каждая запись внутри — под-поле корня группы (widget.icon, widget.color и т. д.). Под-поля неявно наследуют cascade — вам не нужно повторять cascade на каждой строке.

Группа меняет то, как ведут себя переопределения вниз по цепочке:

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

    type service custom {
    widget: my-element // drops parent's widget.icon, widget.color, …
    }
  • Переопределение листа — установка одного листа-члена сохраняет остальную группу.

    type service tweaked {
    widget.color: green // keeps icon: service, subheader: @@team
    }
  • То же значение у корня — повторная привязка корня тем же значением сохраняет группу; полезно, когда вы хотите добавить новые под-поля, не сбрасывая унаследованные.

  • Отбрасывание корняdrop widget удаляет корень и все его под-поля (группа схлопывается, когда корень исчезает).

Под-поля группы могут нести свои собственные модификаторы (required, append), но не cascade (он неявен) и не * (в этой итерации группы не вкладываются).

Аспекты всегда каскадируют-переопределяют

Заголовок раздела «Аспекты всегда каскадируют-переопределяют»

Аспекты (то, что внутри aspect { }) всегда каскадируют с семантикой переопределения. Вы не пишете cascade на аспекте; каскад заложен в само понятие аспекта. Причина: аспекты существуют, чтобы по ним проецировать (проекции группируют по ним и выбирают по ним через show @@…) — аспект, который не течёт, был бы бесполезен для проекции.

service Orders {
aspect { domain: "Orders" }
component Worker {
// Worker.aspect.domain → "Orders" (cascaded)
aspect { domain: "WorkerDomain" }
// Now Worker.aspect.domain → "WorkerDomain" (override)
}
}

Если вам нужно накопление вместо переопределения (список аспектов, растущий от родителя к ребёнку), используйте поле с append, а не аспект. Аспекты не могут композироваться; поля могут.

В большинстве нетривиальных моделей задействованы оба механизма одновременно.

// (A) Type provides defaults and a cascade modifier.
type module service {
required cascade version
component metrics { rest_create emit }
}
// (A) Subtype stamps a fulfilled version and inherits everything else.
type service payments_service {
version: "2.1" // fulfills the blank for descendants
}
// At parse time, mechanism A stamps onto Payments:
// - version: "2.1" (from payments_service)
// - component metrics { rest_create emit } (from service)
// - cascade behavior on version (from service)
payments_service Payments {
"Core payment processing"
component MetricsExporter {
rest_create forward
}
}
// At resolution time, mechanism B walks the structural tree:
// Payments.version → "2.1" (set on Payments itself by stamping)
// Payments.MetricsExporter.version → "2.1" (cascaded from Payments)
// Payments.metrics.version → "2.1" (cascaded from Payments,
// via stamping that put metrics here)
// Payments.metrics.emit → no version field at the interface level,
// resolves to "2.1" via cascade

Экземпляр читается естественно — нет повторяющегося version: на каждом вложенном элементе — потому что механизм 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 (штамповка шаблона). Чтобы выразить «каждый вложенный компонент наследует версию родителя», тип должен был бы знать про каждую возможную глубину вложенности и штамповать version на каждом уровне. Это невозможно, потому что тип не знает, как экземпляры будут вкладываться.

Представьте язык только с механизмом B (структурный каскад). Чтобы выразить «у каждого микросервиса по умолчанию есть компонент metrics», некуда положить это значение по умолчанию — поля каскадируют, а под-объявления — нет.

Каждый механизм делает то, чего не может другой. Вместе они покрывают пространство:

  • A говорит, какие поля и под-объявления существуют на экземплярах.
  • B говорит, как значения текут, когда экземпляры оказались в дереве вложенности.

Нужны оба. Язык даёт оба с различным словарём, чтобы вы могли понять, что именно происходит, когда читаете исходник.

Когда вы делаете drop поля или аспекта в теле, каскадная цепочка разрывается в точке отбрасывания. Потомки не возобновляют чтение из более глубокого предка.

service Orders {
version: "2.4" // cascades
component Special {
drop version // breaks the chain
// Special.version → undefined
// (NOT "walks up past the drop to find the next ancestor")
component Inner {
// Inner.version → undefined (chain still broken)
}
}
}

Это сделано сознательно. drop — это утверждение модели «здесь намеренно нет значения». Если бы каскад просачивался мимо, это молчаливо отменяло бы намерение автора.

  • Есть два механизма распространения: штамповка шаблона (A) и структурный каскад (B).
  • Механизм A работает во время разбора, вдоль цепочки наследования типов. Словарь: наследовать, штамповать.
  • Механизм B работает во время разрешения, вдоль структурного дерева вложенности. Словарь: каскадировать, течь.
  • cascade (переопределение при обходе), append (композирование при обходе) и без модификатора (локальное) — три режима каскада на уровне поля.
  • Аспекты всегда каскадируют с семантикой переопределения. Для накопления используйте поле с append.
  • Два механизма компонуются; правило разрешения конфликтов — вложенное побеждает штампованное.
  • drop абсолютен — он разрывает каскадную цепочку в этой области.

Глава 19: Уточнение, переопределение и отбрасывание → — единый словарь для редактирования того, что предоставляют типы и родители.