12. Пакеты
Пакет — это каталог, содержащий манифест package.archspace и любое количество файлов .arch под ним. Манифест задаёт имя пакета, объявляет его зависимости от других пакетов, указывает на скрипт виджетов и выбирает, какие типы импортируются в область видимости.
package: acme.shopversion: "1.4.0"
dependencies { acme.shared: "../shared" acme.payments: "../packages/payments"}
use service, database, gateway, kafka from arch.backenduse Database, Cache from acme.sharedЭто полноценный манифест. В этой главе разбирается каждое поле, каждое диагностическое сообщение и правила разрешения.
Правило. По умолчанию используйте одно корневое пространство, которое одновременно является пакетом, — держите это как монорепозиторий. Для подавляющего большинства проектов один корневой пакет — правильный ответ. К вложенным пространствам стоит прибегать, только когда владение действительно расходится между организационными подразделениями предприятия (см. Пространства). Пространства проводят линии владения/видимости, а не технические слои.
Анонимные пакеты и однофайловые черновики
Заголовок раздела «Анонимные пакеты и однофайловые черновики»Каталог без манифеста — это анонимный пакет: загрузчик обходит все файлы .arch под ним и разрешает их вместе без имени, без зависимостей, без экспорта.
Отдельный файл .arch — это полноправный, поощряемый способ начать, идеальный для быстрого черновика. Единственное ограничение: один файл может зависеть только от локально определённых типов и типов стандартной библиотеки, но никогда от пользовательских типов из другого пакета (для одинокого файла нет синтаксиса связывания пакетов). Этого с лихвой достаточно — стандартная библиотека огромна — а когда черновик это заслужит, вы повышаете его до управляемого пакета.
package: (обязательное, ровно один раз, в корне пакета)
Заголовок раздела «package: (обязательное, ровно один раз, в корне пакета)»Идентификатор пакета и непрозрачная граница разрешения: зависимые видят только его типы, отмеченные export. Точечная форма принята как соглашение:
package: acme.shop- Минимум один сегмент.
- По соглашению в нижнем регистре, через точку; не принуждается.
- Префикс
arch.*зарезервирован за встроенной стандартной библиотекой. Пользовательский пакет вне каталогаstdlib/инструментария, претендующий наarch.<что-либо>, вызывает ошибкуRESERVED_PACKAGE_NAMESPACE.
Дублирующиеся строки package: — ошибка; побеждает первое вхождение, чтобы у инструментария было нечто стабильное, с чем работать. (name: — другое поле — оно объявляет пространство внутри пакета; см. Пространства. Манифест несёт одно или другое, но не оба сразу.)
Соглашения по именованию точечных сегментов:
- Публикуете типы для потребления другими? Начните с обратного доменного имени компании (
com.acme.payments), чтобы имя было глобально-уникальным адресом. - Только ваш собственный репозиторий? Голое имя компании или проекта подойдёт —
acme.paymentsили простоpayments. Префикс домена не нужен. - Пространства продолжают точечный путь пакета как подпакеты корня (корень
acme→ пространствоacme.retailbanking). - Пакеты стандартной библиотеки сохраняют свои корни:
arch.cloud.aws,arch.extras, …
version: (необязательное, ровно один раз)
Заголовок раздела «version: (необязательное, ровно один раз)»version: "1.4.0"Строка произвольной формы. На данный момент только информационная — загрузчик не разбирает, не сравнивает и не ограничивает версии. Поле зарезервировано на будущее; будущий инструментарий будет использовать диапазоны в стиле semver.
widgets: (необязательное, ровно один раз)
Заголовок раздела «widgets: (необязательное, ровно один раз)»Путь (относительно каталога манифеста) к JS/TS-модулю, который регистрирует виджеты-пользовательские элементы через customElements.define():
widgets: "./widgets.js"Просмотрщик динамически импортирует этот скрипт при запуске и переимпортирует его при изменениях файла (после полной перезагрузки страницы — customElements.define срабатывает один раз для имени тега).
Загрузчик проверяет существование файла во время загрузки. Опечатка немедленно вызывает WIDGETS_FILE_NOT_FOUND. Конвейер виджетов — тема Главы 20.
repo: / commit: (необязательные, закрепление свидетельств)
Заголовок раздела «repo: / commit: (необязательные, закрепление свидетельств)»repo: "https://github.com/acme/shop"commit: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"Репозиторий кода, по которому проверяются привязки-свидетельства sources: этого пакета, и ревизия, относительно которой они истинны. commit: обязателен всегда, когда задан repo: — незакреплённый путь свидетельствует, что файл когда-то существовал, а не что модель истинна относительно коммита. Закрепление на уровне пакета, вместо повторения репозитория рядом с каждой привязкой, — это то, что оставляет голый путь однозначным при любом числе зависимостей у пакета.
Для загрузчика оба поля информационные; их читает проверщик свидетельств. Сами привязки описаны в Главе 37.
dependencies { ... } (необязательное, не более одного блока)
Заголовок раздела «dependencies { ... } (необязательное, не более одного блока)»Отображение имени пакета-зависимости на путь в файловой системе:
dependencies { acme.shared: "../shared" acme.payments: "../../packages/payments"}Для каждой записи загрузчик:
- Разрешает путь относительно каталога манифеста.
- Рекурсивно загружает пакет-зависимость (его собственные зависимости тоже загружаются).
- Проверяет, что
name:загруженного пакета совпадает с именем, объявленным зависящим. Несовпадение вызываетDEP_NAME_MISMATCH.
Ромбовидные зависимости (A→B, A→C, обе →D) загружают D один раз и используют один и тот же экземпляр — загрузчик кэширует по абсолютному пути.
Циклы (A→B, B→A) вызывают DEP_CYCLE для каждого пакета вдоль цикла.
Пакетам arch.* запись не нужна. Загрузчик автоматически разрешает их через встроенную стандартную библиотеку инструментария (настраивается через ARCHLANG_STDLIB).
use … from <pkg> (ноль или более)
Заголовок раздела «use … from <pkg> (ноль или более)»Импортирует типы из зависимости или пакета стандартной библиотеки:
// Импорт одного типаuse database from arch.backend
// Несколькоuse service, frontend from arch.backend
// Wildcard — все экспортированные типы (не рекомендуется; никогда для стандартной библиотеки — см. правило ниже)use * from acme.internal
// Переименованиеuse database as managed_db from arch.backend
// Реэкспорт, чтобы потребители ЭТОГО пакета тоже его виделиexport use payments_provider from acme.payments| Форма | Эффект |
|---|---|
use X from p | Импортирует X напрямую. X должен быть отмечен как export в p — иначе USE_TYPE_NOT_EXPORTED. |
use * from p | Импортирует каждый тип, отмеченный как export в p. |
use X as Y from p | Импортирует X под локальным именем Y. |
export use X from p | Реэкспортирует X, чтобы wildcard-импортёры этого пакета тоже получили X. |
Правило. Предпочитайте явный
use; относитесь к пакету как к палитре. Берите по имени те типы, которые реально нужны, —use postgres, kafka from store.backend, а неuse * from store.backend. Wildcard затаскивает поток неиспользуемых типов и привязывает вас ко всей поверхности пакета.use *оправдан только для небольшой внутренней библиотеки, которую вы написали и хотите целиком; никогда не делайте wildcard для стандартной библиотеки. И объявляйте палитру на уровне пространства (в манифестеpackage.archspace), чтобы она была общей для пространства и обозримой — склоняйтесь к одному общему спискуuse, а не к переимпорту тех же типов файл за файлом.
Область видимости
Заголовок раздела «Область видимости»use, объявленный в корневомpackage.archspace, имеет область пакета: каждый файл.archв этом пакете может ссылаться на импортированные имена.use, объявленный в манифесте пространства (вложенныйpackage.archspaceсname:), имеет область пространства: только файлы в поддереве этого пространства видят эти имена. Словарь остаётся локальным — типpostgres, подключённый черезuseв одном пространстве, не обязан быть назван в другом.use, объявленный внутри файла.arch, имеет область файла: только этот файл может на них ссылаться. Ссылки из соседнего файла вызываютTYPE_NOT_VISIBLE_IN_FILE. (Именно это и заставляет работать однофайловые черновики.)
Внутренние области затеняют внешние; ближайшая выигрывает. Склоняйтесь к уровню пространства: разовая локальная нужда может импортировать рядом с использованием, но один список use на всё пространство самодокументируем — открывший манифест видит всю палитру с первого взгляда.
То же имя, импортированное из того же исходного пакета в нескольких файлах, — нормально; области накапливаются. То же имя из разных исходных пакетов вызывает USE_NAME_COLLISION; переименуйте одно через as.
Анонимные пакеты пропускают проверку export: каждый тип доступен. Как только пакет получает манифест, типы должны быть отмечены export, чтобы быть видимыми зависимым.
Диагностические коды
Заголовок раздела «Диагностические коды»| Код | Когда |
|---|---|
MANIFEST_PARSE_ERROR | Неправильный синтаксис манифеста |
RESERVED_PACKAGE_NAMESPACE | Пакет вне стандартной библиотеки претендует на префикс arch.* |
DEP_LOAD_FAILED | Путь объявленной зависимости не существует или не разбирается |
DEP_NAME_MISMATCH | name: загруженного пакета отличается от ожидания зависящего |
DEP_CYCLE | Цикл в графе зависимостей пакетов |
STDLIB_NOT_FOUND | Импорт arch.* не удалось разрешить через настроенную стандартную библиотеку |
USE_PACKAGE_NOT_FOUND | use ... from <pkg> ссылается на пакет, которого нет ни в dependencies, ни в arch.* |
USE_TYPE_NOT_FOUND | Названный тип не существует в исходном пакете |
USE_TYPE_NOT_EXPORTED | Тип существует, но не отмечен как export |
USE_NAME_COLLISION | Одно и то же локальное имя импортировано из двух разных исходных пакетов |
WIDGETS_FILE_NOT_FOUND | Путь в widgets: не разрешается в существующий файл |
Проработанные примеры
Заголовок раздела «Проработанные примеры»Минимальный черновик:
package: scratchЧерновой проект, который ни от чего не зависит. Каждый файл .arch в каталоге загружается и может импортировать друг из друга; никакие типы arch.* не видны, потому что use не объявлен.
Стандартный проект со стандартной библиотекой:
package: acme.shopversion: "0.4.0"
use service, database, gateway from arch.backenduse rest_create, rest_read, rest_update, kafka, grpc_unary from arch.backendСамая распространённая форма — выверенная палитра встроенных типов модулей (service, database, …) и типов интерфейсов на уровне протокола (rest_create, rest_read, kafka, grpc_unary, …), выбранных по имени, а не через wildcard.
Многопакетный монорепозиторий:
package: acme.appversion: "1.0.0"widgets: "./widgets.js"
dependencies { acme.shared: "../shared" acme.payments: "../packages/payments"}
use service, database, gateway, kafka from arch.backenduse Database, Cache from acme.sharedexport use payments_provider from acme.paymentsОбъявляет зависимости, импорты для всего проекта плюс реэкспорт, чтобы всё, что зависит от acme.app, также подхватывало payments_provider.
Библиотечный пакет:
package: acme.sharedversion: "2.1.0"Библиотека определяет типы в своих файлах .arch и помечает публичные через export. Потребители подключают их по имени через use; неэкспортированные типы остаются внутренними.
Пространства внутри пакета
Заголовок раздела «Пространства внутри пакета»Пространство — это граница видимости внутри пакета, объявляемая вложенным package.archspace, который несёт name: вместо package:. В отличие от границы пакета (непрозрачной, только для типов), пространство прозрачно — одна модель, связи между пространствами по-прежнему видны и ограничиваются только export.
package: akme.bankname: Corporate.LoansРешение определяется владением, а не техническим разделением на слои:
- По умолчанию: одно корневое пространство = пакет = монорепозиторий. Малые и средние системы не платят за границы — держите всё в едином корне.
- Вкладывайте пространства, только когда владение действительно расходится: другой набор людей владеет очень крупным, организационно закрытым доменом — подразделения предприятия за отдельными доменными лидерами, которые почти не сотрудничают. Именно это разделение на уровне организации выражают пространства.
- Не хватайтесь за пространства, чтобы моделировать декомпозицию или слои; это задача вложенности и аспектов, а не границы видимости.
Держите типы рядом с их экземплярами — тип живёт в том же пространстве имён, что и модули, которые его используют, и доменно близко к ним. Запасной выход для крупной организации — выделенный пакет общих типов, вложенный внутрь пакета: непрозрачный, экспортирующий только то, что выбирает сам, и доступный через use откуда угодно. Это даёт один владельческий дом для общего словаря вместо разбросанных или продублированных определений.
Структура файлов
Заголовок раздела «Структура файлов»my-project/├── package.archspace # package: my.project (корень пакета)├── widgets.js # регистрации пользовательских элементов (необязательно)├── orders.arch # домен заказов: модули, процессы, проекции├── business-primitives.arch # пользовательские бизнес-типы для разных модулей└── packages/ └── shared/ ├── package.archspace # вложенный пакет — СВОЯ непрозрачная единица └── lib.archВложенный package.archspace с package: — это жёсткая, непрозрачная граница: обход файлов родителя останавливается на его каталоге, и он разрешается как собственная единица только при ссылке через dependencies или при автозагрузке как arch.*. Вложенный манифест с name: — это прозрачное пространство: обход продолжается сквозь него, модули остаются видимыми через границу как разрешённые рёбра, ограничиваясь только export.
- В каждом проекте есть манифест
package.archspaceв корне, именующий пакет черезpackage:. - Поля:
package:(обязательное),version:,widgets:,dependencies { },use … from …. - По умолчанию используйте один корневой пакет/монорепозиторий; вкладывайте пространства с
name:только для организационных подразделений предприятия, где владение расходится. - Предпочитайте явный
useвместоuse *— берите типы по имени, объявленные на уровне пространства в манифесте; никогда не делайте wildcard для стандартной библиотеки. arch.*зарезервировано за стандартной библиотекой и разрешается автоматически; пользовательским зависимостям нужен путь.useв манифесте пакета имеет область пакета; в манифесте пространства — область пространства; в файле.arch— область файла (именно это и заставляет работать однофайловые черновики).- Ромбовидные зависимости разделяют один экземпляр; циклы вызывают
DEP_CYCLE.
Что дальше
Заголовок раздела «Что дальше»Глава 13: Стабильные идентификаторы → — идентичность, переживающая переименования, и где идентификаторы применимы, а где нет.