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

12. Пакеты

Пакет — это каталог, содержащий манифест package.archspace и любое количество файлов .arch под ним. Манифест задаёт имя пакета, объявляет его зависимости от других пакетов, указывает на скрипт виджетов и выбирает, какие типы импортируются в область видимости.

package: acme.shop
version: "1.4.0"
dependencies {
acme.shared: "../shared"
acme.payments: "../packages/payments"
}
use service, database, gateway, kafka from arch.backend
use 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: "1.4.0"

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

Путь (относительно каталога манифеста) к 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"
}

Для каждой записи загрузчик:

  1. Разрешает путь относительно каталога манифеста.
  2. Рекурсивно загружает пакет-зависимость (его собственные зависимости тоже загружаются).
  3. Проверяет, что name: загруженного пакета совпадает с именем, объявленным зависящим. Несовпадение вызывает DEP_NAME_MISMATCH.

Ромбовидные зависимости (A→B, A→C, обе →D) загружают D один раз и используют один и тот же экземпляр — загрузчик кэширует по абсолютному пути.

Циклы (A→B, B→A) вызывают DEP_CYCLE для каждого пакета вдоль цикла.

Пакетам arch.* запись не нужна. Загрузчик автоматически разрешает их через встроенную стандартную библиотеку инструментария (настраивается через ARCHLANG_STDLIB).

Импортирует типы из зависимости или пакета стандартной библиотеки:

// Импорт одного типа
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_MISMATCHname: загруженного пакета отличается от ожидания зависящего
DEP_CYCLEЦикл в графе зависимостей пакетов
STDLIB_NOT_FOUNDИмпорт arch.* не удалось разрешить через настроенную стандартную библиотеку
USE_PACKAGE_NOT_FOUNDuse ... 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.shop
version: "0.4.0"
use service, database, gateway from arch.backend
use rest_create, rest_read, rest_update, kafka, grpc_unary from arch.backend

Самая распространённая форма — выверенная палитра встроенных типов модулей (service, database, …) и типов интерфейсов на уровне протокола (rest_create, rest_read, kafka, grpc_unary, …), выбранных по имени, а не через wildcard.

Многопакетный монорепозиторий:

package: acme.app
version: "1.0.0"
widgets: "./widgets.js"
dependencies {
acme.shared: "../shared"
acme.payments: "../packages/payments"
}
use service, database, gateway, kafka from arch.backend
use Database, Cache from acme.shared
export use payments_provider from acme.payments

Объявляет зависимости, импорты для всего проекта плюс реэкспорт, чтобы всё, что зависит от acme.app, также подхватывало payments_provider.

Библиотечный пакет:

package: acme.shared
version: "2.1.0"

Библиотека определяет типы в своих файлах .arch и помечает публичные через export. Потребители подключают их по имени через use; неэкспортированные типы остаются внутренними.

Пространство — это граница видимости внутри пакета, объявляемая вложенным package.archspace, который несёт name: вместо package:. В отличие от границы пакета (непрозрачной, только для типов), пространство прозрачно — одна модель, связи между пространствами по-прежнему видны и ограничиваются только export.

akme/package.archspace
package: akme.bank
akme/corporate/loans/package.archspace
name: 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: Стабильные идентификаторы → — идентичность, переживающая переименования, и где идентификаторы применимы, а где нет.