4. Модули
Модуль — это архитектурная единица с одним ответственным сопровождающим, чёткой границей и публичной поверхностью, состоящей из интерфейсов. Это та примитивная сущность, к которой крепится всё остальное.
module Payments { aspect team: "Payments" "Authorizes and captures card payments."
interface authorize interface capture interface refund}Это модуль. Тип — module; имя Payments — в UpperCamelCase; аспект team указывает, кто им владеет; описание и три интерфейса в lowerCamelCase раскрывают, что он делает.
Эта и несколько следующих глав используют только базовые типы языка — module, surface, interface. Стандартная библиотека добавляет более богатые типы вроде service, database, command, event (см. Главу 11). Всё, что есть в этой главе, применимо и к ним; они являются подтипами module.
Правило
Заголовок раздела «Правило»Правило. Если у этого есть владелец и граница — это модуль.
Это весь тест. Сервисы — модули. Базы данных — модули. Внешние системы — модули. Люди и внешние клиенты (актёры) — модули. Подсистемы, содержащие другие модули, — модули. «Библиотека», лежащая внутри репозитория одной команды, со своей контрактной поверхностью, — модуль.
Если вы ловите себя на вопросе «это модуль или что-то другое?» — и у этого есть владелец и граница — это модуль.
Насколько глубоко погружаться
Заголовок раздела «Насколько глубоко погружаться»Вы можете вкладывать модули бесконечно — хоть модуль на каждый класс, если захочется. Не стоит. Код текуч, поэтому глубоко промоделированная архитектура быстро устаревает, и каждый уровень детализации — это то, к чему придётся возвращаться и обновлять.
Правило. Соразмеряйте глубину моделирования масштабу системы — и не глубже.
Микросервис — вполне хороший модуль корневого уровня; часто лучше описать его хорошо, чем дробить на тривиальные подмодули. Монолит заслуживает нескольких вложенных модулей; сервис-ориентированная система — нескольких на сервис. Рекомендуемый нижний предел — один модуль на доменную единицу самого низкого уровня (фичу); на этом остановитесь. Модуль-на-класс переносит структуру кода на архитектуру, и это неверный выбор по умолчанию: архитектура — не программирование. Когда подмодуль был бы слишком мелким, чтобы себя оправдать, отразите эту деталь в описании.
Базовый тип module
Заголовок раздела «Базовый тип module»Базовый тип — module. У него нет обязательных полей, нет интерфейсов по умолчанию и нет специального виджета. Это самое общее возможное объявление модуля:
module Orders { interface createOrder interface cancelOrder}Это полноценный модуль. Два интерфейса, ни команды, ни описания, ни аспектов. Валидатор его принимает. Рендерер диаграммы рисует его как подписанный прямоугольник.
Большинство архитектур не используют базовый module напрямую. Они используют тип из стандартной библиотеки, такой как service, или тип, определённый в проекте, такой как payment_service — оба являются подтипами module, которые добавляют требования к команде, виджеты и соглашения. Но базовая форма всегда доступна, и именно к ней сводится каждый более богатый тип.
Вложенность
Заголовок раздела «Вложенность»Модули могут содержать другие модули. Используйте любую из форм:
Прямая вложенность:
module Platform { aspect team: "Platform Engineering"
module AuthService { interface authenticate }
module UserService { interface getUser }}Плоская через in:
module Platform { aspect team: "Platform Engineering"}
in Platform module AuthService { interface authenticate}
in Platform module UserService { interface getUser}Обе формы дают идентичную структуру. Форма с in позволяет держать объявление родителя в одном файле и даёт командам добавлять дочерние модули из своих собственных файлов, не редактируя все одно и то же место, — в этом распределённом авторстве и есть смысл.
Viewer отрисовывает вложенные модули как контейнеры. module Platform становится рамкой; AuthService и UserService становятся узлами внутри неё.
Вложенность означает вложенность доменов
Заголовок раздела «Вложенность означает вложенность доменов»Поместить B внутрь A — это утверждение: B принадлежит домену A, это его под-часть, а не равноправный элемент, с которым он общается. Поэтому вкладывайте по домену, а не по тому, кто-кого-вызывает. AuthService живёт внутри Platform, потому что он часть домена платформы, а не потому, что кто-то его вызывает.
Именно это делает ресурсы с единственным владельцем простыми. Хранилище данных, используемое ровно одним модулем, — это технологическая деталь домена этого модуля, поэтому оно вкладывается внутрь:
module Orders { aspect team: "Commerce" interface createOrder
module OrderStore { // Orders' own datastore — encapsulated interface read interface write }}Правило. Ресурс, которым владеет ровно один модуль, вкладывается внутрь него. Ресурс, разделяемый несколькими модулями, — равноправный элемент; объявляйте его как соседа на том же уровне.
Не рисуйте хрестоматийную картинку с рядом сервисов, каждый из которых подключён к своей коробке-базе. База с единственным владельцем инкапсулирована своим владельцем; только разделяемая база — это соседний узел. (Где хранилище размещено — СУБД, сервер — это уже другая плоскость, прикрепляемая аспектом, а не вложенностью; Глава 9 это покрывает.)
Владение следует той же вложенности. Аспект team, заданный на родителе, распространяется на его детей, если ребёнок его не переопределяет, так что отмечайте владение только там, где оно действительно меняется — Orders несёт aspect team: "Commerce", а OrderStore его наследует.
Безымянные модули: имя из типа
Заголовок раздела «Безымянные модули: имя из типа»Когда тип вложенного модуля уже говорит всё то, что сказало бы его имя, имя можно опустить. Вложенный модуль с пользовательским типом, записанный без имени (и без идентификатора), автоматически берёт имя своего типа, преобразованное из lower_snake_case в UpperCamelCase:
service SomeService { database // без имени → модуль с именем «Database» payment_processor // без имени → модуль с именем «PaymentProcessor» cache Redis // всё ещё можно дать настоящее имя, когда хочется}Это сокращение «нет смысла называть очевидное» — особенно для случая использует библиотеку: пишете тип библиотеки, пропускаете имя. (database, cache, payment_processor здесь — пользовательские типы или типы из стандартной библиотеки — Глава 11 и Глава 16 — а не базовый тип module.)
Правила достаточно строги, чтобы выводимое имя всегда было однозначным:
- Только пользовательские типы. У базового типа
moduleнет имени типа, которое можно было бы позаимствовать, поэтому голыйmoduleвсегда должен быть назван. - Один безымянный на тип, на родителя. Тип можно оставить безымянным, только если он использован в родителе ровно один раз. Два ребёнка с типом
databaseне могут оба быть безымянными — назовите оба. Но ребёнокdatabaseи ребёнокcacheмогут оба быть безымянными рядом, поскольку их применённые типы различаются (а подтип считается своим собственным типом). - Только модули. Интерфейсы и поверхности не берут имя из своего типа.
- Нет идентификатора до повышения. Безымянный модуль остаётся без идентификатора; назвав его позже (или когда форматтер его повысит) — вот тогда он зарабатывает стабильный идентификатор.
Ссылайтесь на безымянного ребёнка по его выводимому имени — это и есть его имя: Orders > Database.write.
Правило. Безымянный-по-типу модуль — не
TODO. Он полностью назван и полностью разрешён — это сокращение в именовании, а не черновой долг. (В отличие от анонимного процесса или интерфейса, которые являютсяTODO— Глава 7.)
Тело модуля — это в основном поля. Поле имеет вид key: value:
module Payments { aspect team: "Payments" repo.url: "https://github.com/acme/payments" version: v2 ext.cmdb.ci: "CI28304858" "Core payments service"}Ключи полей могут быть точечными (repo.url, ext.cmdb.ci). Значения — идентификаторы (Payments, v2), строки в кавычках, числа или булевы. Это весь язык значений — никаких вложенных объектов, никаких типов на поле. Глава 9 подробно покрывает поля.
Голая строка "Core payments service" — это особое поле: описание. В теле модуля может быть несколько описаний; они склеиваются. Глава 10 посвящена им.
Интерфейсы
Заголовок раздела «Интерфейсы»Интерфейсы — это то, как модули показывают себя другим модулям. Внутри тела модуля объявления интерфейсов соседствуют с полями:
module Orders { aspect team: "Commerce"
interface createOrder interface cancelOrder interface getOrder interface orderEvents}Ключевое слово interface — это базовый тип интерфейса. Стандартная библиотека определяет подтипы вроде command, query и event, которые несут семантическую информацию (синхронный или асинхронный, чтение или запись). Пока что каждый интерфейс — это просто interface. Глава 5 подробно разбирает интерфейсы; Глава 11 знакомит с типами интерфейсов из стандартной библиотеки.
Интерфейсы всегда листовые: интерфейс не содержит другой интерфейс. Чтобы их сгруппировать, используйте поверхности — Глава 6.
Стабильные идентификаторы
Заголовок раздела «Стабильные идентификаторы»После того как вы сохраните файл .arch через форматтер, каждый модуль получает стабильный идентификатор:
module #m4k29p Payments { aspect team: "Payments"}Префикс #m4k29p закрепляет идентичность модуля сквозь переименования. Переименуйте Payments в PaymentsService — идентификатор останется тем же, и инструменты вместе с диффами понимают, что это тот же модуль. Идентификатор намеренно непрозрачен: он не кодирует ни имени, ни смысла, поэтому его никогда не нужно менять. Глава 13 подробно возвращается к этому. Пока что: не пишите идентификаторы вручную; пусть их выпускает форматтер.
Пустые тела
Заголовок раздела «Пустые тела»Если модулю нечего добавить, тело можно полностью опустить:
module NotificationsЭто валидно. Модуль существует без полей и без интерфейсов. Полезно как заглушка или когда сама идентичность модуля и есть весь архитектурный факт.
- Модуль — это всё, у чего есть владелец и граница.
- Базовый тип —
module. Каждый более богатый тип (service, database, user, …) является его подтипом. - Тела содержат поля, описания, интерфейсы, поверхности и вложенные модули — в любом порядке.
- Модули могут вкладываться напрямую или прикрепляться к объявленному родителю через
in Parent. - Стабильные идентификаторы (
#xyz) закрепляют идентичность при переименованиях; форматтер выпускает их при сохранении.
Что дальше
Заголовок раздела «Что дальше»Глава 5: Интерфейсы → — как модули показывают себя друг другу.