Граф приложения как архитектура¶
2 сентября 2026
Внедрение зависимостей обычно представляют как механизм удобства: вместо ручного конструирования объектов фреймворк конструирует их и поставляет их зависимости. Это описание корректно, но в Kora Framework оно останавливается слишком рано.
В Kora граф приложения — не просто деталь реализации внедрения зависимостей. Это компактная архитектурная модель работающего сервиса. Граф описывает, какие части приложения существуют, какие части от каких зависят, какие модули участвуют в финальном сервисе, какие компоненты владеют жизненным циклом, какие части могут инициализироваться параллельно, какие компоненты должны освобождаться первыми во время завершения и как инфраструктура, такая как телеметрия, подключена к остальной системе.
Это различие важно, потому что Kora строит и валидирует большую часть этого графа на этапе компиляции. Фреймворк не ждёт запуска приложения, чтобы просканировать classpath, обнаружить определения, согласовать конкурирующих кандидатов и только затем узнать, можно ли собрать сервис. Компилятор и процессоры аннотаций Kora уже знают достаточно о приложении, чтобы генерировать обычный исходный код, представляющий его связывание.
Результат — полезное архитектурное свойство:
Исходный код
↓
компоненты + модули + явные зависимости
↓
граф времени компиляции
↓
сгенерированное связывание приложения
↓
жизненный цикл рантайма
Одна и та же модель участвует во всех этих стадиях. Поэтому граф гораздо ближе к исполняемому описанию архитектуры, чем к традиционному локатору сервисов времени выполнения.
Эта статья рассматривает эту идею с архитектурной точки зрения. Смысл не в повторении API внедрения зависимостей Kora, а в показе того, что становится возможным, когда структура приложения явна, типизирована, сгенерирована и валидирована до запуска процесса.
Сервис уже является графом¶
Любой нетривиальный бэкенд-сервис естественно является ориентированным графом, признаёт это фреймворк или нет.
Рассмотрим небольшой HTTP-сервис:
HTTP-сервер
↓
Роутер
↓
PetController
↓
PetService
↙ ↘
PetRepository AuditService
↓ ↓
DataSource Kafka Producer
У каждого узла есть роль. Каждое ребро говорит нечто более сильное, чем «у объекта A случайно есть поле типа B». Ребро утверждает, что одна часть системы требует другую часть, чтобы выполнять свою функцию.
Эти рёбра одновременно кодируют несколько видов архитектуры:
- архитектуру конструирования — что должно существовать, прежде чем другой компонент может быть создан;
- поведенческую архитектуру — какие возможности компонент делегирует другим компонентам;
- архитектуру жизненного цикла — какие ресурсы должны быть инициализированы до их потребителей и освобождены после того, как эти потребители остановятся;
- модульную архитектуру — какие технологические или доменные модули становятся частью сервиса;
- эксплуатационную архитектуру — как метрики, трассировка, логирование, проверки, серверы, базы данных, клиенты обмена сообщениями и другая инфраструктура подключаются к коду приложения.
Многие фреймворки в конце концов строят такой граф внутри. Архитектурное различие в том, когда граф становится известен, насколько явно он описан и может ли разработчик рассуждать о нём до рантайма.
Ответ Kora — перенести как можно больше этой работы в компиляцию.
От внедрения зависимостей к исполняемой модели архитектуры¶
Приложение Kora начинается с интерфейса, аннотированного @KoraApp.
На первый взгляд это выглядит как точка входа внедрения зависимостей. Архитектурно, однако, оно говорит значительно больше.
Приложение явно объявляет, что конфигурация, логирование, метрики и HTTP-сервер Undertow принадлежат этому сервису. Если другая интеграция не подключена, Kora не обнаруживает молча и инициализирует её просто потому, что какая-то зависимость случайно присутствует в classpath. Внешние модули подключаются намеренно через интерфейс приложения.
Бизнес-код добавляет ещё один слой структуры:
Конструктор выражает два ребра графа:
Фабрики выражают то же самое явно через параметры методов:
Ключевая архитектурная идея в том, что Kora не нужен отдельный DSL архитектуры для обнаружения базовой структуры сервиса. Обычные типы, конструкторы, интерфейсы, модули и фабричные методы Java или Kotlin уже несут достаточно информации, чтобы описать большую её часть.
Это держит архитектурные объявления близко к исполняемому коду. Когда меняется зависимость, архитектура меняется в той же правке.
Граф — это архитектура зависимостей¶
Архитектура зависимостей отвечает на простой вопрос:
Что от чего зависит?
Это звучит тривиально, пока система не станет достаточно большой, чтобы ответ больше не был виден из одного файла.
В контейнере с тяжёлым рантаймом понимание этой структуры может требовать её мысленной реконструкции из нескольких источников: аннотаций, сканирования компонентов, классов конфигурации, условных регистраций, пост-процессоров рантайма, соглашений фреймворка, правил автоконфигурации, а иногда и динамических прокси.
Kora намеренно сужает это пространство. У компонентов есть явные провайдеры. Зависимости типизированы. Внешние модули явно подключены. Отсутствующие провайдеры, неоднозначные провайдеры, недопустимые теги и многие циклы — проблемы построения графа, которые всплывают во время компиляции.
Предположим, контроллер требует сервис:
и сервис требует репозиторий:
Если у PetRepository нет провайдера, это не просто «конфигурация DI сломана». Архитектурно граф неполон:
Диагностика компилятора Kora может сообщить путь разрешения зависимости от корня вниз до отсутствующего запроса. Это важно, потому что ошибка — не только ошибка типа; это объяснение того, как был достигнут сломанный архитектурный путь.
Хороший граф времени компиляции превращает такие отказы в обратную связь об архитектуре:
То же применимо к неоднозначности. Если две реализации удовлетворяют одну и ту же зависимость и ничто их не различает, архитектура недоопределена. Теги, значения по умолчанию или явные выборы провайдера заставляют эту неоднозначность разрешаться в исходном коде.
Это одна из причин, по которой DI времени компиляции имеет ценность за пределами производительности запуска. Он превращает набор архитектурных инвариантов в проверяемые компилятором свойства.
Граф — это также жизненный цикл¶
Графы объектов часто обсуждают так, будто это статические диаграммы. Производственные приложения не статичны. Они запускаются, приобретают ресурсы, принимают трафик, перезагружают выбранное состояние, перестают принимать работу, сбрасывают или освобождают ресурсы и завершаются.
Как только компоненты владеют ресурсами, направление зависимостей становится направлением жизненного цикла.
Представьте эту структуру:
Сервис не может осмысленно стать готовым, прежде чем готов репозиторий, который ему нужен, а репозиторий не может работать, пока его источник данных не пригоден к использованию.
Поэтому инициализация следует за графом:
Завершение должно обратить его:
Контейнер рантайма Kora использует граф зависимостей для инициализации компонентов в порядке зависимостей, используя параллелизм везде, где это позволяют независимые ветви. Компоненты с поддержкой
жизненного цикла
реализуют Lifecycle, а обёртки могут присоединять поведение жизненного цикла к значениям, произведённым фабриками. При завершении компоненты освобождаются в обратном порядке зависимостей; интеграции
фреймворка, такие как
HTTP-серверы и потребители Kafka, используют ту же механику жизненного цикла для корректного завершения.
Это делает граф планом исполнения для запуска и завершения.
Рассмотрим две независимые ветви:
┌─→ JdbcDataSource ─→ PetRepository ─┐
Application Root │ ├─→ PetService
└─→ Kafka Producer ─→ AuditService ──┘
Ветвь базы данных и ветвь Kafka не должны инициализироваться серийно просто потому, что принадлежат одному приложению. Граф уже говорит рантайму, какие узлы независимы. Kora может поэтому инициализировать как можно больше параллельно и синхронизироваться только там, где этого требуют рёбра зависимостей.
Это тонкая, но важная точка: граф зависимостей — также карта конкурентности для инициализации приложения.
Рантайму не нужно угадывать, какие шаги инициализации независимы. Архитектура уже содержит эту информацию.
Рёбра зависимостей имеют семантику жизненного цикла¶
Kora делает архитектурное значение рёбер ещё яснее через прямые и косвенные зависимости.
Обычная зависимость означает больше, чем «дай мне этот объект». Она также связывает изменения жизненного цикла. Если компонент C напрямую зависит от компонента B, и граф обновляет B, то C —
часть
затронутого поддерева зависимостей и также может нуждаться в пересборке.
Поэтому прямое ребро означает приблизительно:
ValueOf<T> намеренно ослабляет это отношение. Потребитель получает живую ссылку, из которой может получить текущее значение графа, но сам потребитель не должен пересоздаваться
просто потому, что этот ссылочный узел меняется.
Архитектурно это разные рёбра.
Это различие особенно полезно для долгоживущей инфраструктуры. HTTP-сервер владеет сокетом и не должен разбираться каждый раз, когда меняется граф обработчиков запросов. Сервер может оставаться стабильным, наблюдая обновлённые обработчики через косвенную ссылку.
Это превращает граф из простого DAG конструирования объектов в более богатую модель, выражающую границы распространения.
Полезный способ думать об этом:
Прямая зависимость
= структурная + связность жизненного цикла
ValueOf / PromiseOf
= структурная ссылка со сниженной связностью жизненного цикла
Это даёт архитекторам конкретный механизм для выражения того, какие изменения должны распространяться через работающую систему, а какие нет.
Граф определяет порядок инициализации без отдельного скрипта запуска¶
Традиционные сервисы часто накапливают код запуска со временем:
загрузить конфигурацию
инициализировать логирование
создать метрики
открыть пул базы данных
запустить миграции
создать репозитории
запустить фоновые рабочие
привязать HTTP-сервер
пометить приложение готовым
В конце концов это становится собственным слоем оркестрации, с написанными вручную правилами порядка и обработкой отказов. Граф зависимостей может естественно кодировать большую часть этого порядка: если HTTP-сервер зависит от роутера, роутер зависит от контроллеров, контроллеры зависят от сервисов, а сервисы зависят от баз данных или клиентов, то граф уже говорит, что должно существовать до обработки трафика. Система жизненного цикла Kora использует эту структуру напрямую, а не требует параллельного описания запуска.
Это снижает риск двух конкурирующих архитектурных моделей:
Когда эти модели раздельны, они могут расходиться. Разработчик добавляет новую зависимость, но забывает обновить порядок запуска. Ресурс запускается слишком рано. Хук завершения закрывает зависимость до того, как её потребитель остановился.
Когда топология зависимостей и жизненного цикла разделяет один граф, объём дублирующегося архитектурного знания уменьшается.
Границы модулей становятся видимыми в графе¶
Граф также описывает, откуда берутся возможности приложения.
Модули Kora — обычные интерфейсы, предоставляющие фабрики. Внешние модули не активируются глобально просто потому, что их JAR появляется в classpath; приложение подключает те, которые намеревается использовать.
Например:
Этот верхнеуровневый интерфейс — лаконичное архитектурное заявление:
Application
├── Конфигурация
├── Инфраструктура базы данных JDBC
├── Домен Pet
├── Домен Audit
└── HTTP-инфраструктура
Для более крупных кодовых баз @KoraSubmodule предоставляет ещё одну полезную границу. Gradle-модуль может владеть доменным срезом, собирать свои определения @Component и @Module и раскрывать
этот срез финальному
графу приложения.
Концептуально:
application
│
├── PetSubmodule
│ ├── PetController
│ ├── PetService
│ ├── PetRepository
│ └── JDBC-интеграция
│
└── VetSubmodule
├── VetController
├── VetService
└── VetRepository
Это больше, чем организация кода. Модуль, никогда не подключённый к приложению, не может тихо стать частью работающего сервиса через широкое обнаружение classpath.
Это свойство помогает поддерживать архитектурные границы, потому что участие в финальном приложении явно.
Это также создаёт полезную поверхность проверки. Изменение, такое как:
public interface Application extends
PetModule,
VetModule,
+ BillingModule,
UndertowPublicHttpServerModule {
}
явно является архитектурным изменением. Сервис приобрёл целую новую возможность. Это легче заметить при проверке, чем транзитивную автоконфигурацию, молча активирующуюся, потому что зависимость появилась в файле сборки.
Граф включает инфраструктуру, а не только бизнес-сервисы¶
Частая ошибка при рисовании архитектурных диаграмм — показывать только бизнес-компоненты:
Реальная производственная система содержит гораздо больше:
┌─→ Метрики
├─→ Трассировка
HTTP-сервер → Route ├─→ Контекст логирования
└─→ Controller → Service → Repository → DataSource
Kafka Consumer → Handler → Service
System Server → /metrics
→ /system/liveness
→ /system/readiness
Граф Kora включает эти инфраструктурные компоненты как обычные части композиции приложения.
Стек наблюдаемости — хороший пример. Подключение модулей метрик, трассировки, логирования и HTTP добавляет конкретные объекты и отношения в граф: реестр метрик, сбор метрик, инфраструктуру трассировки, системные эндпоинты, серверные компоненты и специфичные для приложения сервисы, которые испускают бизнес-сигналы.
Это архитектурно ценно, потому что наблюдаемость — не невидимый побочный канал, прикрученный к приложению постфактум. Она часть собранной системы.
Можно думать о графе как о содержащем две переплетённые архитектуры:
Бизнес-архитектура
---------------------
Controller
Service
Repository
Доменные клиенты
Эксплуатационная архитектура
------------------------
HTTP-сервер
Конфигурация
Метрики
Трассировка
Логирование
Проверки
Планировщики
Потребители Kafka
Пулы баз данных
Производственное поведение возникает из обеих.
Сервис, чьи бизнес-зависимости корректны, но чьё связывание телеметрии, готовности, завершения или конфигурации неверно, всё равно архитектурно сломан. Рассмотрение инфраструктуры как первоклассных узлов графа делает эти отношения легче для рассуждения.
Связывание наблюдаемости — архитектурное связывание¶
Модель наблюдаемости Kora демонстрирует, почему граф приложения — лучшая абстракция, чем «мешок бинов».
Полный наблюдаемый сервис может подключать:
Результирующий граф может содержать отношения, такие как:
MeterRegistry
↓
телеметрия фреймворка
↓
метрики HTTP / базы данных / обмена сообщениями
экспортёр OTel
↓
tracer
↓
спаны запросов / спаны клиентов / бизнес-спаны
системный HTTP-сервер
↓
сборщик метрик + liveness + readiness
С архитектурной точки зрения это отвечает на вопросы, которые часто отсутствуют на статических диаграммах:
- Откуда берётся реестр метрик?
- Какой сервер раскрывает эксплуатационные эндпоинты?
- Какая реализация телеметрии внедряется в модуль базы данных?
- Какой tracer используется HTTP-клиентами и бизнес-кодом?
- Какие проверки вносят вклад в готовность?
- Какая реализация логирования несёт корреляцию трассировки?
Если эти отношения живут в том же графе, что и бизнес-зависимости, о наблюдаемости можно рассуждать структурно, а не рассматривать как магию фреймворка.
Это также делает кастомизацию более локальной. Замена фабрики телеметрии по умолчанию или предоставление специфичного для приложения компонента — подстановка в графе, а не совершенно другой механизм расширения.
Валидация графа времени компиляции становится валидацией архитектуры¶
Самое сильное архитектурное следствие подхода Kora в том, что некоторые ошибки дизайна становятся ошибками компиляции.
Это не значит, что компилятор может доказать, что ваша архитектура хороша. Он не может сказать, есть ли у сервиса корректные ограниченные контексты, элегантна ли ваша доменная модель или оправдана ли зависимость концептуально.
Что он может сделать — валидировать структурные инварианты, которые часто являются предпосылками здравой архитектуры.
Отсутствующие зависимости¶
Если контроллер в конечном счёте требует репозиторий, который не предоставлен, граф не может быть завершён.
Это обнаруживается до развёртывания.
Неоднозначные зависимости¶
Если два компонента реализуют один и тот же контракт, и точка внедрения не говорит, какой ей нужен, граф неоднозначен.
Теги или другой явный выбор разрешают архитектуру.
Недопустимые или случайные циклы¶
Циклы часто указывают на тесную связанность:
Kora может обнаруживать циклы графа во время компиляции. Некоторые циклы на основе интерфейсов можно обрабатывать с помощью сгенерированных обещанных прокси, а разработчики могут намеренно ослаблять
связность жизненного цикла через
ValueOf или PromiseOf. Важная часть архитектурно в том, что цикл видим как свойство графа, а не всплывает как таинственный отказ инициализации рантайма.
Отсутствующие корни¶
Граф без корневых компонентов фактически не имеет исполняемой поверхности приложения. Kora может отклонить такую форму, а не молча строить ничего полезного.
Несовпадения тегов¶
Теги — типизированные архитектурные квалификаторы. Если граф содержит корректный контракт под другим тегом, чем запрошенный, несовпадение видимо компилятору, и диагностика может указывать на близлежащих кандидатов.
Каждая из этих проверок переносит обратную связь раньше:
вместо:
архитектурная ошибка
↓
собрать образ
↓
развернуть
↓
запустить приложение
↓
отказ во время обнаружения рантайма
Практическая выгода не только в надёжности. Она меняет экономику архитектурных экспериментов. Рефакторинг, ломающий инварианты графа, даёт обратную связь, пока разработчик ещё в цикле правка-компиляция.
Диагностика становится путями через архитектуру¶
Полезная диагностика графа не должна просто говорить:
Более полезный вопрос:
Почему приложению вообще понадобился
PetRepository?
Поскольку граф известен, диагностика может объяснить путь разрешения зависимости:
Это фактически небольшой архитектурный след.
Для больших приложений это имеет большое значение. Компонент, который терпит неудачу, может быть на несколько слоёв удалён от корня, который вызвал его включение. Отсутствующий маппер может требоваться репозиторием, который требуется сервисом, который требуется HTTP-контроллером. Показ пути отвечает и на что отсутствует, и на почему компилятор пытался это построить.
Тот же принцип применяется к нескольким кандидатам:
Диагностика раскрывает архитектурное решение, которое не было принято.
Этот стиль обратной связи — одна из причин, по которой сгенерированное связывание времени компиляции хорошо работает и с людьми, и с агентами кодирования: компилятор возвращает конкретное структурное объяснение, которое можно проследить обратно в исходный код.
Сгенерированное связывание — поверхность архитектурной отладки¶
Kora генерирует обычный исходный код, а не скрывает построение графа за рефлексией или генерацией байткода рантайма.
Это создаёт ещё одно полезное свойство: сгенерированный код проверяем.
Когда поведение неясно, разработчик может пройти по стеку:
бизнес-класс
↓
конструктор / фабрика
↓
сгенерированное связывание графа
↓
фактическая интеграция фреймворка
Это уменьшает разрыв между архитектурой, которую вы думали, что объявили, и архитектурой, которую рантайм фактически исполняет.
Сгенерированный исходный код может отвечать на такие вопросы, как:
- Какая фабрика создаёт этот компонент?
- Какая реализация удовлетворила этот интерфейс?
- Какой тег разрешил эту зависимость?
- Какой сгенерированный адаптер сидит между контроллером и HTTP-сервером?
- Какой конкретный фабричный компонент телеметрии был связан в эту интеграцию?
- Почему компонент вообще был включён в граф?
В модели рантайма с тяжёлой рефлексией некоторые из этих ответов существуют только после того, как контейнер запустился и завершил динамическое обнаружение. Со сгенерированным связыванием большая часть ответа существует как код до запуска сервиса.
Это особенно полезно во время обновлений фреймворка. Если генерация меняется, разработчики могут изучить новый вывод и сравнить фактическое связывание, а не рассматривать контейнер как чёрный ящик.
Граф — естественная основа для визуализации¶
Как только у приложения есть статически известный граф, визуализация становится проблемой проецирования данных, а не упражнением в рантайм-археологии.
Концептуальное преобразование простое:
граф Kora
↓
метаданные узлов + рёбер
↓
фильтровать / группировать / аннотировать
↓
диаграмма архитектуры
Например, сырой граф компонентов можно отобразить как:
UndertowServer
↓
HttpRouter
↓
PetController
↓
PetService
↙ ↘
PetRepository AuditService
↓ ↓
JdbcDataSource KafkaProducer
Более полезный вид архитектуры мог бы группировать технические узлы:
┌──────────────── HTTP ────────────────┐
│ Undertow → Router → PetController │
└──────────────────┬───────────────────┘
↓
┌────────────── Business ──────────────┐
│ PetService ─────────→ AuditService │
└──────────────┬──────────────┬────────┘
↓ ↓
┌──────────── Data ────┐ ┌ Messaging ┐
│ Repository → JDBC │ │ Kafka │
└──────────────────────┘ └────────────┘
Другой вид мог бы показывать только зависимости жизненного цикла. Другой мог бы выделять межмодульные рёбра. Другой мог бы показывать корни и транзитивные подграфы, достижимые из них. Другой мог бы схлопывать внутренности фреймворка и оставлять только компоненты приложения.
Текущая документация Kora раскрывает доступ к графу и сгенерированные структуры графа, но архитектурная точка здесь шире любой одной встроенной команды визуализации: граф времени компиляции даёт инструментам стабильную модель для визуализации.
Это открывает дверь полезным инженерным инструментам, таким как:
- экспорт в Graphviz или Mermaid;
- исследование графа в IDE;
- архитектурные диффы в pull-request;
- отчёты о границах модулей;
- обнаружение неожиданно центральных компонентов;
- визуализация критического пути жизненного цикла;
- анализ размера графа и веера;
- проверки политик зависимостей в CI.
Важная оговорка в том, что не всё это — заявления о функциях, которые Kora сейчас поставляет. Это естественные возможности инструментов, ставшие возможными благодаря явному наличию графа.
Архитектурная валидация может выходить за рамки «Можно ли построить граф?»¶
Как только архитектура представлена как узлы и рёбра, валидация может стать богаче, чем стандартное разрешение зависимостей.
Первый уровень уже присутствует в Kora:
существует ли провайдер?
уникален ли провайдер?
совместимы ли теги?
разрешим ли цикл?
достижим ли корень?
конструируем ли компонент?
Но граф также может поддерживать специфичные для организации правила.
Представьте кодовую базу с этими архитектурными слоями:
Команда может решить, что репозитории никогда не должны зависеть от HTTP-контроллеров, или что один доменный модуль не может напрямую зависеть от модуля персистентности другого домена.
Эти политики — правила графа:
разрешено: Controller → Service
разрешено: Service → Repository
запрещено: Repository → Controller
запрещено: DomainA → DomainBInternalRepository
Точно так же эксплуатационные требования можно формулировать структурно:
Каждый публичный HTTP-сервер должен иметь трассировку.
Каждая база данных должна иметь телеметрию.
Каждый долгоживущий потребитель должен участвовать в жизненном цикле.
Ни один бизнес-компонент не может напрямую зависеть от Graph.
Инфраструктурные модули могут зависеть от общих контрактов,
но доменные модули не должны зависеть от специфичных для фреймворка HTTP-типов.
Граф приложения времени компиляции делает такие политики гораздо легче оценивать, чем контейнер, чья финальная топология существует только динамически после запуска.
Это одно из самых интересных долгосрочных следствий DI времени компиляции: контейнер зависимостей может стать субстратом анализа архитектуры.
Граф может раскрывать архитектурные запахи¶
Даже без сложных инструментов политик сама форма графа может раскрывать проблемы.
Чрезмерный веер внутрь¶
Компонент с огромным числом зависимостей может делать слишком много или координировать слишком много забот.
Чрезмерный веер наружу¶
Это может указывать, что оркестрация принадлежит сервису приложения, а не напрямую адаптеру.
Междоменные рёбра¶
Если задуманная архитектура говорит, что Orders должен общаться с Billing через публичный контракт, граф может раскрыть случайный ярлык.
Утечка инфраструктуры¶
Граф может сделать утечку фреймворка в доменный код видимой.
Горячие точки жизненного цикла¶
Если часто обновляемый узел конфигурации вызывает пересборку очень большого поддерева прямых зависимостей, граф раскрывает, что несколько границ ValueOf могли бы изолировать долгоживущую
инфраструктуру от
изменчивой конфигурации.
Это архитектурные запахи, а не универсальные ошибки. Фреймворк не должен слепо их отклонять. Но явный граф делает их наблюдаемыми и измеримыми.
Срезы графа улучшают тестирование¶
Граф приложения также меняет то, как можно понимать компонентные тесты.
Вместо конструирования совершенно другой вселенной зависимостей для тестов поддержка тестирования Kora может взять производственный граф приложения и сохранить тестируемый компонент вместе с транзитивными зависимостями, нужными для этого среза, применяя тестовые замены там, где необходимо.
Концептуально:
Тест, ориентированный на сервис, может работать с меньшим графом:
Это архитектурно сильнее, чем ручная сборка тестового графа объектов, который лишь приблизительно повторяет производственное связывание. Тест основан на тех же отношениях компонентов и той же модели жизненного цикла, только срезан и заменён там, где требуется.
Это даёт графу ещё одну роль:
он становится общей структурной моделью, разделяемой производственным запуском и компонентным тестированием.
Когда тесты используют реальную структуру графа, архитектурные рефакторинги с большей вероятностью ломают тесты полезными способами. Если сервис приобретает новую требуемую зависимость, тестовый граф должен её учесть. Тестовая настройка не может молча дрейфовать так далеко от производственного связывания.
Обновление рантайма показывает, что граф — не просто статические метаданные¶
Граф времени компиляции не значит, что приложение никогда не может меняться после запуска.
Kora разделяет две заботы:
время компиляции: валидировать и генерировать структуру графа
рантайм: держать значения, управлять жизненным циклом, обновлять затронутые узлы
Граф рантайма может обновить узел и пересобрать компоненты, которые напрямую от него зависят. Обновление выполняется атомарно: затронутые компоненты подготавливаются во временном состоянии графа, и только после успешной инициализации новое состояние заменяет старое. Если инициализация терпит неудачу, временные объекты освобождаются, а существующий граф остаётся активным.
Архитектурно это интересно, потому что топология времени компиляции становится правилом для распространения изменений рантайма.
Предположим, меняется конфигурация:
Обновление, начинающееся с Config, может распространяться через прямые рёбра зависимостей.
Но если стабильный сервер использует ValueOf<Handler>:
то обработчик может меняться без пересборки сервера, владеющего сокетом.
Поэтому граф моделирует не только архитектуру-в-покое, но и архитектуру-под-изменением.
Почему это важно для отладки¶
Производственная отладка часто начинается с симптомов, а не структуры:
«Эндпоинт возвращает 503.»
«Kafka перестал потреблять.»
«Готовность никогда не становится здоровой.»
«Новая конфигурация не вступает в силу.»
«Сервис зависает во время завершения.»
Граф даёт каждому симптому структурный контекст.
Для падающего эндпоинта:
Для проблемы телеметрии:
Для проблемы завершения:
Граф говорит вам, какие компоненты всё ещё могут использовать ресурс, когда начинается освобождение, и каким должен быть корректный обратный порядок зависимостей.
Для проблемы обновления он говорит вам, прямое ли отношение или опосредовано через ValueOf, что определяет, должен ли потребитель пересобираться или только наблюдать новое значение.
Это гораздо более действенно, чем думать о DI как о механизме поиска. Отладка становится навигацией по типизированной топологии системы.
Почему это важно для разработки с поддержкой нейро-технологий¶
Те же свойства, которые помогают инженерам рассуждать о графе, помогают агентам кодирования.
Агент работает лучше всего, когда может отвечать на детерминированные вопросы из исходного кода:
Где создаётся этот компонент?
От чего он зависит?
Какая реализация выбрана?
Какой модуль его добавляет?
Каков путь от корня приложения к этому компоненту?
Что перезапустится, если этот узел будет обновлён?
Какой сгенерированный код выполняет связывание?
Граф времени компиляции делает эти вопросы сравнительно конкретными.
Петля обратной связи также полезна:
агент редактирует архитектуру
↓
компилятор строит граф
↓
Kora валидирует структуру
↓
точная диагностика
↓
агент исправляет код
Это гораздо меньшее пространство поиска, чем отладка контейнера, чьё финальное поведение зависит от обнаружения рантайма, рефлексии, порядка конфигурации и скрытых правил активации.
Есть ещё одно преимущество: сгенерированные источники может проверять модель. Вместо угадывания того, что произведут рантайм-прокси или метаданные рефлексии, агент может читать код, который сгенерировал Kora, и следовать фактическим вызовам конструкторов и фабрик.
В этом смысле граф становится и моделью архитектуры, и машиночитаемым объяснением архитектуры.
Полезная мысленная модель: граф — скелет сервиса¶
Лаконичный способ думать о Kora такой:
Скелет определяет, какие части соединяются, что что поддерживает, где существуют границы и как вся система может безопасно собираться и разбираться.
Более конкретно:
Граф приложения
├── топология зависимостей
├── выбор провайдеров
├── композиция модулей
├── порядок жизненного цикла
├── параллелизм инициализации
├── порядок завершения
├── распространение обновлений
├── эксплуатационное связывание
└── тестируемые срезы графа
Это гораздо больше архитектурной ответственности, чем обычно подразумевает фраза «контейнер внедрения зависимостей».
Граф времени компиляции Kora меняет роль DI¶
Традиционное внедрение зависимостей часто ощущается как инфраструктура, сидящая рядом с архитектурой приложения.
Вы проектируете архитектуру сначала, затем настраиваете контейнер для её инстанцирования.
Kora движется к другой модели:
архитектура, выраженная в типизированном коде
↓
построение графа
↓
валидация времени компиляции
↓
сгенерированное связывание
↓
жизненный цикл рантайма
Граф — мост, соединяющий эти стадии.
У этого несколько следствий.
Во-первых, объявления зависимостей становятся архитектурными объявлениями. Добавление параметра конструктора — не просто запрос ещё одного объекта у контейнера; это меняет топологию зависимостей и потенциально топологию жизненного цикла.
Во-вторых, композиция модулей становится видимой на границе приложения. Добавление инфраструктурного модуля — явное архитектурное изменение.
В-третьих, запуск и завершение выводятся из той же структуры, что и связывание зависимостей, а не поддерживаются как параллельная модель оркестрации.
В-четвёртых, диагностика может описывать сломанную архитектуру как пути зависимостей.
В-пятых, сгенерированный код предоставляет конкретную поверхность отладки.
Наконец, граф может стать входом для инструментов, которые валидируют, визуализируют, сравнивают и анализируют архитектуру.
Как мог бы выглядеть рабочий процесс, ориентированный на граф¶
Как только команды начинают рассматривать граф приложения как архитектуру, рабочий процесс разработки может эволюционировать за пределы «компилируется ли DI?».
Зрелый рабочий процесс мог бы выглядеть так:
1. Разработчик меняет компоненты или модули
↓
2. Kora строит граф на этапе компиляции
↓
3. Выполняется структурная валидация
↓
4. Сгенерированное связывание компилируется
↓
5. Инструменты архитектуры анализируют граф
↓
6. Тесты выполняются против срезов графа / полного графа
↓
7. CI сравнивает архитектурные изменения
Стадия анализа архитектуры могла бы отвечать на такие вопросы, как:
Добавил ли этот PR новую междоменную зависимость?
Стал ли компонент хабом зависимостей?
Протекла ли инфраструктурная сущность в доменный слой?
Стал ли критический путь жизненного цикла длиннее?
Стал ли новый модуль частью приложения?
Получило ли обновление конфигурации неожиданно большой радиус поражения?
Опять же, не каждый пункт в этом списке — встроенная функция Kora сегодня. Смысл в том, что граф времени компиляции Kora делает такой стиль инструментов практичным, потому что архитектура уже существует в структурированной форме.
Разница между диаграммой архитектуры и исполняемым графом¶
Обычная диаграмма архитектуры полезна, но пассивна.
Ничто не заставляет реализацию продолжать ей соответствовать. Исполняемый граф другой. Если реализация говорит:
то это ребро не просто присутствует на диаграмме — оно присутствует в коде, построении графа, топологии жизненного цикла и сгенерированном связывании.
Это не устраняет потребность в диаграммах более высокого уровня. Сырой граф приложения может содержать сотни инфраструктурных узлов и сгенерированных адаптеров, слишком детальных для архитектурной коммуникации.
Но он даёт этим диаграммам более высокого уровня заслуживающий доверия источник, из которого их можно выводить.
Идеальное отношение:
а не:
Это более крупная архитектурная возможность за графами зависимостей времени компиляции.
Заключение¶
Граф приложения Kora не следует рассматривать только как механизм, заменяющий контейнер внедрения зависимостей рантайма.
Это структурное ядро приложения.
Он описывает зависимости. Эти зависимости определяют порядок инициализации. Та же топология определяет порядок освобождения. Прямые и косвенные рёбра определяют распространение обновлений. Модули определяют, какие возможности принадлежат сервису. Интеграции наблюдаемости становятся явными частями собранной системы. Тесты могут работать на срезах того же графа. Сгенерированное связывание делает структуру проверяемой. Валидация времени компиляции превращает многие структурные дефекты в обратную связь компилятора.
Это даёт графу несколько одновременных ролей:
модель зависимостей
+
модель жизненного цикла
+
композиция модулей
+
эксплуатационное связывание
+
диагностическая модель
+
субстрат инструментов
=
архитектура приложения
Важный сдвиг концептуален. Внедрение зависимостей само по себе не интересная часть. Интересная часть — наличие типизированной, явной модели времени компиляции того, как собран сервис.
Как только эта модель существует, компилятор может её валидировать, рантайм может её исполнять, разработчики могут её проверять, тесты могут её срезать, инструменты могут её визуализировать, а правила архитектуры можно оценивать против неё.
Вот почему граф приложения — одна из самых важных архитектурных идей в Kora: он превращает структуру приложения из чего-то, что фреймворк обнаруживает во время выполнения, в то, что инструментальная цепочка разработки может понять до того, как сервис когда-либо запустится.