Архитектура приложения
Приложение Wippy — это не дерево исходных файлов, а граф записей реестра. Код живёт в записях function.lua и process.lua; всё, что их связывает — какая функция отвечает на HTTP-маршрут, каким процессом управляет тот или иной сервис, какая библиотека что импортирует, — объявляется в _index.yaml. Структурировать приложение — значит решить, как разрезать этот граф на пространства имён, чтобы по мере роста он оставался компонуемым, тестируемым и загружаемым.
Эта страница — обоснование такой раскладки. Механические правила (формат файлов, именование, где лежит _index.yaml) — см. YAML и структура проекта. Сами типы записей — см. Руководство по типам записей.
Единица — это слайс
Организуйте код по фичам, а не по типам файлов. Слайс владеет одной возможностью целиком — её доступом к базе данных, её долгоживущими процессами, её HTTP-поверхностью и общим для них словарём — и живёт под одним префиксом пространства имён:
src/app/jobs/ namespace: app.jobs
src/app/auth/ namespace: app.auth
src/app/billing/ namespace: app.billing
Альтернатива — верхнеуровневое деление на handlers/, models/, services/ — размазывает каждую фичу по всему дереву и связывает их через соседство. Слайсы держат радиус поражения фичи внутри одной папки: её можно прочитать, протестировать или удалить, не выискивая ссылки по всему проекту.
Слои внутри слайса
Внутри слайса делите по оси что касается внешнего мира. Это архитектура портов и адаптеров (гексагональная), выраженная под-пространствами имён:
src/app/jobs/ namespace: app.jobs ← общий словарь
consts.lua config.lua types.lua
persist/ namespace: app.jobs.persist ← адаптеры базы данных (sql)
service/ namespace: app.jobs.service ← процессы, воркеры
api/ namespace: app.jobs.api ← http.endpoints
Импорты текут только в одну сторону, от внешнего к внутреннему:
api → service → persist → { consts, config, types }
Корень слайса (общий словарь) ничего не импортирует из собственных детей. Дети импортируют корень. Ни один слой не тянется обратно вверх, и ни один слайс не импортирует другой слайс напрямую — совместное использование между слайсами идёт через общее родительское пространство имён (например, app.core:types), никогда вбок.
Слайс поменьше сворачивает церемонию — одного _index.yaml с библиотеками и одним эндпоинтом вполне достаточно. Правило, которое выживает при любом размере, — направление импортов, а не количество папок.
Общий словарь
Три файла повторяются в корне хорошо структурированного слайса. Они содержат то, что читает каждый слой, но чем ни один из них не является:
| Файл | Содержит | Возможности |
|---|---|---|
consts.lua |
Конечные автоматы, перечисления, ярусы очередей, registry ID процессов. Значения, зеркалящие CHECK-ограничения вашей базы данных. |
нет |
config.lua |
Настраиваемые через env параметры с кодовыми значениями по умолчанию (env.get(KEY) or DEFAULT), так что запись env.variable не обязательна для того, чтобы значение было необязательным. |
env |
types.lua |
Формы сущностей (type Job = { ... }) — строки, которые возвращает слой персистентности. |
нет |
consts и types не объявляют никаких host-возможностей — это чистые library.lua, возвращающие таблицу. Это сознательно: ваш доменный словарь не может выполнять I/O, поэтому не может дрейфовать в бизнес-логику и юнит-тестируется без базы данных и без process host.
Держите этот словарь приватным для слайса. Константы и типы, общие для нескольких слайсов, живут в общем родителе и используются через импорт оттуда — и никогда не копируются в каждый слайс.
Возможности сортируются по слоям
Каждая запись объявляет нужные ей host-возможности в modules:. В слоистом слайсе они раскладываются чисто:
persist/*объявляетsql— и ничто другое не получает доступ к базе данных.service/*объявляетchannelи возможности process host — и ничто другое не порождает процессы и не супервизирует.api/*объявляет то, что нужно эндпоинту для маршалинга запроса.- Корневой словарь не объявляет ничего.
Выгода в том, что радиус поражения любой возможности — ровно один слой. Если нужно знать всё, что может писать в базу данных, читайте persist/. Инверсия зависимостей перестаёт быть абстрактным принципом и становится свойством, которое можно найти grep'ом.
Приложения и компоненты
Та же форма масштабируется от одиночного приложения до опубликованной библиотеки, меняя только того, кто заполняет дыры.
Приложение — верхнеуровневый развёртываемый граф. Оно владеет конкретной инфраструктурой — http.service, process.host, соединением с базой данных — под корневым пространством имён (по конвенции app) и связывает всё само.
Компонент — публикуемый модуль, монтируемый в хост. Он не может назвать базу данных или роутер хоста, потому что не знает их. Вместо этого он объявляет интерфейс дыр — записи ns.requirement, которые хост заполняет, когда зависит от компонента. Внутри компонент устроен ровно как слайс приложения: те же слои, тот же словарь, то же направление импортов. Единственное добавление — интерфейс требований на его границе.
Это спектр, а не две категории:
- Одно приложение, внутренние слайсы — слайсы живут под
src/app/и напрямую разделяют инфраструктуру приложения, ссылаясь наapp:db,app:processes. Интерфейс требований не нужен; ничто внешнее их не монтирует. (Так строится сфокусированный сервис.) - Композиция из нескольких компонентов — каждый компонент является собственным публикуемым модулем с
ns.definitionи интерфейсом изns.requirement, компонуемым хостом черезns.dependency. Хост заполняет каждое требование (база данных, process host, роутер) один раз. (Так строится платформа из переиспользуемых частей.)
Выбирайте по тому, предназначен ли слайс для потребления чем-то, что вы не контролируете. Если да — дайте ему интерфейс требований и опубликуйте. Если нет — пусть он ссылается на инфраструктуру приложения напрямую и обходится без церемоний. Слоистость — инвариант на обоих концах; вместе с переиспользованием масштабируется именно упаковка.
См. Создание компонентов о механизме requirement/dependency и Управление зависимостями о стороне lock-файла.
Почему такая форма {#why-this-shape}
Дисциплина выше — не стиль. Каждое правило несущее для того, как среда выполнения компонует и загружает граф:
Граница пространства имён — это шов внедрения. Поскольку слои связываются только через явные imports: и живут в разных пространствах имён, у механизма ns.requirement есть конкретная цель для внедрения — хост направляет свою базу данных в записи слоя persist, свой process host — в записи слоя service. Если бы persist напрямую хватал app:db, компонент нельзя было бы смонтировать в другой хост: не было бы дыры, которую можно заполнить. Слоистость — то, что делает компонент перемещаемым.
Однонаправленные импорты гарантируют существование порядка загрузки. Среда выполнения разрешает граф записей при старте и должна найти топологический порядок. api → service → persist → корень, никогда вбок и никогда вверх, означает, что граф ацикличен по построению. Меж-слайсовая связность, проведённая через общего родителя, оставляет слайсы независимо монтируемыми, вместо того чтобы завязать их в цикл, который загрузчик не сможет упорядочить.
Возможности, ограниченные слоем, ограничивают радиус поражения. Host-возможности выдаются на каждую запись. Когда sql объявляет только persist, множество кода, способного дотянуться до базы данных, — одна директория, проверяемая одним взглядом, а не эмерджентное свойство всего приложения.
Слоистость даёт градиент тестируемости. Чистый словарь тестируется без внешнего мира. Тесты persist задевают базу данных, но не воркер. Тест монтирования целого модуля затем проверяет швы, которые юнит-тесты сознательно не видят: что каждый супервизируемый сервис указывает на реальный процесс, каждый порождаемый ID разрешается, каждое требование заполнено. Этот градиент возможен, только если слои действительно разделимы.
Коротко: гексагональная слоистость здесь — единственная форма, при которой одновременно работают внедрение требований, ограничение возможностей по слоям и ацикличное разрешение загрузки. Модель композиции среды выполнения требует разделения на порты и адаптеры, чтобы функционировать, — дисциплина и есть то, что даёт вам граф, который загружается, и компонент, который может смонтировать кто-то другой.
См. также
- YAML и структура проекта — формат файлов, именование, пространства имён
- Создание компонентов —
ns.definition,ns.requirement, монтирование - Управление зависимостями — lock-файлы, потребление модулей
- Реестр — как записи хранятся и разрешаются
- Типы записей — все типы записей
- Модель процессов — сервисы, супервизия, хосты