Сильный выбор по умолчанию — это не привязка: Kora создан для расширения¶
1 августа 2026
Мнение-ориентированные фреймворки часто оцениваются через ложную бинарность.
Либо фреймворк предоставляет много конкурирующих способов решить одну и ту же проблему — и тогда его называют гибким, либо он предоставляет один рекомендованный путь — и тогда он рискует быть названным ограничивающим. Подразумевается, что свобода приходит от числа видимых альтернатив, а привязка начинается, как только фреймворк говорит: «вот так мы рекомендуем это делать».
Это не полезное определение расширяемости.
Фреймворк может раскрывать десять точек расширения и всё равно быть трудным для настройки, если реализация по умолчанию глубоко встроена во внутренности времени выполнения. Он может поддерживать несколько моделей программирования и всё равно делать базовый компонент почти невозможным для замены без понимания приватных правил бутстрапа. Он может рекламировать широкую плагинную систему, заставляя кастомный код входить в отдельный слой интеграции, который ведёт себя иначе, чем код, принадлежащий фреймворку.
Верно и обратное.
Фреймворк может предоставлять один сильный, мнение-ориентированный выбор по умолчанию, оставаясь высоко расширяемым, если этот выбор по умолчанию — просто одна реализация внутри публичной модели приложения. Если альтернативную реализацию можно добавить через тот же граф зависимостей, если сгенерированный компонент может уступить другой реализации того же контракта, если модули опциональны и если кастомные компоненты участвуют в том же жизненном цикле, валидации зависимостей, телеметрии и правилах композиции, что и встроенные компоненты, тогда фреймворк мнение-ориентирован, но не закрыт.
Это важное различие во фреймворке Kora.
Kora намеренно пытается сократить число конкурирующих способов решения общих бэкенд-проблем. Его философия «одна проблема — одно решение» не означает, что альтернативные реализации запрещены. Она означает, что фреймворк хочет один ясный путь по умолчанию, который команды могут принять, не тратя дни на выбор среди пересекающихся внутренних API. Выбор по умолчанию — рекомендованный путь, а не граница того, что приложению разрешено делать.
Архитектура остаётся открытой.
Упрощённый взгляд выглядит так:
Закрытый фреймворк
реализация по умолчанию
↓
внутренности фреймворка
↓
трудно заменить
↓
форк / обходной путь / глубокий API расширения
Kora стремится к другой модели:
Kora
реализация по умолчанию
↓
публичный контракт
↓
заменить / расширить / добавить модуль
↓
тот же явный граф приложения
Это приводит к центральному аргументу статьи:
Фреймворк по-настоящему расширяем не потому, что у него много точек расширения. Он расширяем, когда кастомный код участвует в той же модели, что и встроенный код.
Расширяемость Kora сильнее всего именно потому, что он не создаёт отдельный мир для расширений. Кастомный компонент не входит через скрытый плагинный реестр. Кастомной интеграции не нужно обходить внедрение зависимостей. Заменяющая реализация не требует форка фреймворка. Локально написанный модуль можно компоновать теми же механизмами, что и модули, поставляемые самим Kora.
Это гораздо более сильная форма расширяемости, чем большой каталог специальных хуков.
Мнение-ориентированный не означает закрытый¶
Есть две разные идеи, которые часто путают:
и:
Это не одно и то же.
Мнение-ориентированный выбор по умолчанию говорит:
Мы выбрали один подход, который хорошо работает для общего случая. Он документирован, протестирован, интегрирован и поддерживается как путь, с которого большинству пользователей следует начинать.
Обязательная реализация говорит:
Остальная часть фреймворка предполагает, что эта точная реализация существует, и её замена требует борьбы с фреймворком.
Первое может снижать когнитивную нагрузку.
Второе создаёт привязку.
Философия дизайна Kora явно ближе к первой.
Фреймворк пытается предоставить связный выбор по умолчанию для общих бэкенд-забот, чтобы командам не приходилось выбирать среди нескольких пересекающихся API для каждого слоя. Это ценно, потому что у бэкенд-систем уже достаточно реальных решений: топология базы данных, проектирование API, согласованность, конкурентность, обработка сбоев, масштабирование, владение схемой, наблюдаемость, развёртывание и безопасность. Фреймворк не обязательно делает разработчиков продуктивнее, создавая дополнительные внутренние выборы для всех этих забот.
Если три API фреймворка выполняют примерно одну и ту же работу, командам нужно ещё одно решение:
Затем ещё одно:
Затем ещё одно:
Затем ещё одно:
Стоимость — не только документация.
Это организационная фрагментация.
Философия Kora «один ясный путь» пытается устранить этот класс решений, где возможно. Это делает путь по умолчанию более узким, но архитектура всё равно может оставаться открытой под ним.
Различие таково:
Это ключ к пониманию модели расширяемости Kora.
Граф приложения — это поверхность расширения¶
Граф зависимостей Kora — не только механизм связывания компонентов фреймворка.
Это архитектурная поверхность, через которую встречаются код приложения, код фреймворка, сгенерированный код и сторонние интеграции.
Это важно, потому что избегает распространённой проблемы фреймворков: встроенные компоненты живут в одном привилегированном мире, а пользовательские расширения — в другом.
Приложение Kora может содержать:
встроенный компонент Kora
сгенерированный компонент
@Component приложения
фабрику из @Module
клиент сторонней библиотеки
кастомную реализацию
внутреннюю интеграцию компании
С точки зрения графа все они — компоненты.
Их происхождение менее важно, чем их контракт и зависимости.
Это означает, что кастомная реализация может участвовать в той же структуре:
точно так же, как встроенная реализация может участвовать в:
Эта симметрия — сердце расширяемости.
Кастомный компонент не терпится как исключение. Он становится частью обычной модели зависимостей.
Опциональные модули меняют значение слова «фреймворк»¶
Большие фреймворки часто обретают гравитационное притяжение.
Вы добавляете одну высокоуровневую зависимость фреймворка, и эта зависимость приносит большой набор возможностей, транзитивных библиотек, авто-конфигурацию, механизмы обнаружения, инфраструктуру времени выполнения и интеграции по умолчанию. Даже если приложение использует лишь малую их часть, фреймворк устанавливает широкую среду времени выполнения.
Kora выбирает более модульный подход.
Приложение явно подключает модули, которые ему нужны.
Маленькое приложение может выглядеть примерно так:
Если сервису позже понадобятся метрики:
Если нужен экспортёр OpenTelemetry, этот модуль добавляется явно.
Если нужны Kafka, gRPC, JDBC, S3, планирование, кеширование или другая интеграция, эти возможности входят в приложение, потому что проект их выбирает.
Это меняет ментальную модель с:
на:
Это важная форма расширяемости, потому что самый лёгкий для замены компонент — часто тот, который в первую очередь вообще не был насильно вставлен в приложение.
Минимальное ядро, инкрементальный стек¶
Хороший фреймворк должен позволять архитектуре сервиса расти согласно фактическим требованиям.
Модульная модель Kora поддерживает это.
Раннему сервису может понадобиться только:
Позже он может добавить:
Ещё позже:
Фреймворк не требует, чтобы приложение брало на себя огромный платформенный след с первого дня.
Получившийся паттерн роста:
У этого два архитектурных преимущества.
Во-первых, поверхность зависимостей остаётся намеренной. Технология входит в приложение, потому что кто-то её добавил.
Во-вторых, расширяемость не требует замены монолитного рантайма. Приложение уже скомпоновано из модулей, поэтому кастомный модуль естественно вписывается в ту же форму.
Встроенные модули — примеры той же модели¶
Сильнейшая модель расширения — та, в которой авторы фреймворка используют по сути те же публичные механизмы композиции, которые ожидают использовать разработчики приложений.
Архитектура модулей Kora близка к этому идеалу.
Интеграция фреймворка обычно предоставляет компоненты через интерфейсы модулей. Код приложения подключает эти модули к @KoraApp. Кастомная интеграция также может раскрывать компоненты через
интерфейс модуля и подключаться к тому же приложению.
Концептуально:
Встроенный модуль
FrameworkModule
├── конфигурация
├── компонент по умолчанию
├── телеметрия
└── жизненный цикл
↓
граф приложения
Кастомный модуль может выглядеть так:
CustomModule
├── конфигурация
├── кастомный клиент
├── телеметрия
└── жизненный цикл
↓
граф приложения
Важно то, чего не появляется между ними:
специальный рантайм расширения
менеджер плагинов
сервис-локатор
реестр рефлексии
кастомный API бутстрапа
Это отсутствие делает архитектуру легче для анализа.
@Module мощнее, чем API плагинов¶
Традиционная плагинная система часто предоставляет специфичный для фреймворка интерфейс:
Кастомному коду теперь приходится понимать:
- когда происходит регистрация;
- как реестр разрешает зависимости;
- в каком порядке выполняются плагины;
- как управляется жизненный цикл;
- видны ли зарегистрированные объекты обычному DI;
- как осуществляется доступ к конфигурации;
- как работает тестирование;
- применима ли ещё валидация компилятора.
Это создаёт вторую модель композиции.
Фабрика @Module в Kora концептуально гораздо проще.
Она говорит:
Например:
Компонент входит в тот же граф, что и всё остальное.
Компилятор может валидировать, существуют ли SearchConfig и SearchTelemetry.
Сервис может инжектировать SearchClient.
Другой компонент может декорировать его или зависеть от него.
Тесты могут его заменять.
Можно подключить жизненный цикл.
Расширение — не боковой канал.
Это внедрение зависимостей.
Именно эта простота делает механизм мощным.
Кастомные и встроенные компоненты разделяют один статус¶
Это одно из самых важных свойств по-настоящему расширяемого фреймворка.
Предположим, приложение содержит:
HTTP-сервер Kora
сгенерированный репозиторий
кастомный фрод-клиент
встроенную реализацию метрик
кастомную реализацию фича-флагов
сгенерированный OpenAPI-клиент
внутренний модуль аудита
Фреймворк с привилегированными встроенными компонентами может представлять это внутренне как:
Граф Kora позволяет более чистую модель:
Граф приложения
├── HTTP-сервер
├── репозиторий
├── фрод-клиент
├── метрики
├── фича-флаги
├── OpenAPI-клиент
└── сервис аудита
Источник компонента вторичен.
Важно:
- его тип;
- его теги, где релевантно;
- его зависимости;
- его жизненный цикл;
- его место в графе.
Вот почему фраза «первоклассная кастомная интеграция» значима.
Первоклассная не означает, что есть аннотация под названием @Extension.
Это означает, что кастомному коду не нужно покидать обычную модель приложения.
Сильные значения по умолчанию должны быть заменяемы на границе контракта¶
Наиболее полезная реализация по умолчанию — та, которая выбрана за стабильным контрактом.
Kora использует @DefaultComponent в нескольких модулях ровно для такого поведения.
Компонент по умолчанию доступен, когда приложение не предоставило более сильную альтернативу. Код приложения может предоставить другую реализацию того же требуемого контракта и позволить этой реализации стать выбранным компонентом.
Важная архитектурная форма:
и:
Потребитель зависит от контракта.
Ему не должно быть важно, какая реализация победила.
Это очень отличается от значения по умолчанию, связанного глубоко внутри внутренностей фреймворка.
Рассмотрим генерацию тегов телеметрии. Документация метрик Kora раскрывает компоненты провайдеров тегов по умолчанию для HTTP, gRPC, Kafka и других интеграций. Приложения могут предоставлять собственную реализацию соответствующего контракта провайдера, когда стандартный маппинг тегов не подходит.
Это конкретный пример более широкого принципа расширяемости.
У фреймворка есть значение по умолчанию.
Значение по умолчанию — не единственная возможная реализация.
Почему @DefaultComponent — важный сигнал дизайна¶
Ценность @DefaultComponent больше, чем сама аннотация.
Она кодирует архитектурное отношение:
Фреймворк готов предоставить эту реализацию, но признаёт, что приложение владеет финальным графом.
Это здоровая модель владения.
Фреймворк может сказать:
Приложение может сказать:
Приложению не нужно форкать модуль метрик.
Оно должно предоставить другой компонент, реализующий публичный контракт.
Тот же паттерн применим везде, где фреймворк может чисто отделить политику от механизма.
Это снижает риск того, что значения по умолчанию фреймворка станут организационными ограничениями.
Сгенерированный код не должен означать сгенерированную привязку¶
Генерация кода на этапе компиляции может создавать другой вид привязки, если сгенерированные реализации рассматриваются как неприкосновенные внутренности.
Так быть не должно.
Сгенерированный код наиболее гибок, когда сгенерированный класс реализует или удовлетворяет обычный контракт, который потребляет остальная часть графа.
Представьте:
Ключевой вопрос дизайна — не в том, была ли реализация сгенерирована.
Ключевой вопрос — зависит ли код приложения от:
или от:
Если граница — контракт, можно предоставить другую реализацию.
Это даёт:
Сервис приложения по-прежнему зависит от того же интерфейса.
Поэтому генерация — удобство и оптимизация, а не обязательно клетка.
Это различие важно, потому что генерацию кода иногда описывают как делающую фреймворки жёсткими. На практике сгенерированный код может фактически увеличивать гибкость, если он создаёт обычные типизированные реализации, а не скрытые объекты времени выполнения.
Замените реализацию, сохраните граф¶
Мощный паттерн расширяемости — замена одной части приложения без изменения остальной его архитектуры.
Предположим, сервис начинается со стандартной реализации:
Позже организации нужен специализированный транспорт.
Закрытый фреймворк мог бы заставить:
Граф-ориентированный фреймворк должен позволять:
Остальная часть графа не заботится.
Это суть инверсии управления.
Фреймворк не должен владеть конкретным объектом просто потому, что предоставил первый.
Расширяемость сильнее всего, когда валидация всё ещё работает¶
Скрытый запасной выход обычно ослабляет гарантии.
Например, если фреймворк валидирует связывание приложения на этапе компиляции, но кастомные расширения входят через реестр времени выполнения, эти расширения могут обходить основную модель безопасности.
Это создаёт два уровня уверенности:
встроенный граф
→ валидирован на этапе компиляции
кастомный плагин
→ валидирован во время выполнения
Теперь у приложения есть слабый шов ровно там, где происходит настройка.
Модель графа Kora избегает этого во многих случаях, потому что кастомные компоненты всё ещё являются компонентами графа.
Если кастомной фабрике @Module нужны зависимости, которые не могут быть разрешены, процессор графа может отклонить приложение.
Если кастомная реализация создаёт неоднозначный контракт, об этой неоднозначности можно сообщить.
Если через кастомный код появляется цикл зависимостей, граф всё равно может о нём рассуждать.
Поэтому расширение остаётся внутри сильнейшей границы валидации фреймворка.
Это гораздо лучше, чем плагинная система, которая говорит:
Вы можете расширять что угодно, но как только вы это сделаете, вы сами по себе.
Расширяемость должна сохранять жизненный цикл¶
Построение зависимостей — только одна часть производственного фреймворка.
Кастомный компонент может владеть:
- потоками;
- соединениями;
- сокетами;
- пулами;
- фоновыми воркерами;
- наблюдателями;
- каналами.
Если расширения первоклассны, они должны уметь участвовать в запуске и завершении приложения.
Модель жизненного цикла Kora позволяет кастомным компонентам это делать.
Кастомный клиент можно сконструировать через модуль и обернуть в поведение жизненного цикла, если необходимо.
Концептуально:
конфигурация
↓
кастомный модуль
↓
клиент
↓
инициализация
↓
использование приложением
↓
корректное освобождение
Это важно, потому что многие фреймворки легко расширять в момент создания объекта, но трудно расширять операционно.
Объект можно зарегистрировать.
Но порядок завершения приватный.
Готовность отдельна.
Телеметрия отдельна.
Тестирование использует другую модель.
Истинная расширяемость включает операционное поведение.
Расширяемость должна сохранять наблюдаемость¶
Кастомный компонент по-настоящему первоклассен в продакшене, только если его можно наблюдать.
Модель приложения Kora позволяет кастомным интеграциям потреблять или раскрывать ту же инфраструктуру телеметрии, которую используют встроенные компоненты.
Кастомный клиент, похожий на базу данных, может зависеть от:
или от специфичной для проекта абстракции телеметрии.
Кастомный модуль может подключить их во время построения.
Кастомный компонент также может предоставлять пробы готовности или живости, если эта зависимость влияет на доступность приложения.
Это даёт согласованную операционную форму:
встроенная интеграция
├── конфигурация
├── жизненный цикл
├── телеметрия
└── компонент графа
кастомная интеграция
├── конфигурация
├── жизненный цикл
├── телеметрия
└── компонент графа
Эта симметрия важнее числа явных API расширения.
Разница между расширяемостью и запасными выходами¶
Документация фреймворков часто перечисляет «точки расширения» как свидетельство гибкости.
Но точка расширения может означать многое.
Здоровая точка расширения выглядит так:
Нездоровый запасной выход выглядит так:
неподдерживаемый случай использования
↓
специальный реестр
↓
внутренний хук фреймворка
↓
недокументированный порядок
↓
надежда, что обновления его не сломают
Оба технически позволяют настройку.
Только один устойчив.
Сильнейший аргумент в пользу подхода Kora в том, что многие настройки вообще не являются расширениями в экзотическом смысле.
Это обычные компоненты.
Это резко снижает объём специальных знаний фреймворка, которые требуются.
Кастомные модули — это архитектура, а не сантехника фреймворка¶
Предположим, у компании есть внутренний сервис фича-флагов.
Его можно интегрировать так:
Код приложения инжектирует клиент.
Если несколько сервисов используют одну интеграцию, модуль может переехать во внутреннюю библиотеку.
Результат может стать:
Это не хак вокруг фреймворка.
Это архитектура, выраженная с помощью нативной модели композиции фреймворка.
Это различие важно, потому что внутренним платформенным командам часто нужны десятки специфичных для компании интеграций, которые ни один публичный фреймворк не может разумно поставлять.
Правильному фреймворку не нужно знать эти системы заранее.
Ему нужно сделать их интеграцию обычной.
Расширение через композицию стабильнее, чем расширение через внутренности¶
Внутренности фреймворка меняются.
Приватные классы бутстрапа меняются.
Реестры рефлексии меняются.
Внутренности жизненного цикла меняются.
Реализации компилятора меняются.
Расширение, которое зависит от этих деталей, имеет высокую стоимость обновления.
Композиция через публичные контракты стабильнее.
Если кастомный компонент зависит только от:
публичного контракта конфигурации
публичного контракта графа
публичного контракта жизненного цикла
публичного контракта телеметрии
тогда внутренности фреймворка могут эволюционировать, не заставляя расширение эволюционировать с той же скоростью.
Это одна из причин, по которой явное внедрение зависимостей — такая сильная модель расширения.
Зависимости видны на уровне типов.
Расширение связано с тем, что оно фактически использует.
Одна проблема — одно решение — это организационная оптимизация¶
Фраза «одна проблема — одно решение» может звучать догматично, если понимать её буквально.
Лучшая интерпретация организационная.
Представьте фреймворк, поддерживающий три официальные модели HTTP-клиента:
У каждой есть:
- собственные аннотации;
- собственная конфигурация;
- собственное поведение телеметрии;
- собственная обработка ошибок;
- собственная история тестирования;
- собственный жизненный цикл;
- собственная документация.
Команды теперь делают локальные выборы.
Через несколько лет:
Команда A → Клиент A
Команда B → Клиент B
Команда C → Клиент C
Легаси-сервис → Клиент A v1
Новый сервис → Клиент C
У организации больше нет одной модели фреймворка.
У неё несколько под-фреймворков.
Поверхность сопровождения умножается.
Предпочтение Kora одной рекомендованной модели снижает эту энтропию.
Но если выбранная модель действительно не может решить требование, граф всё равно должен позволять другую реализацию.
Этот баланс важен:
Это, пожалуй, лучшее определение мнение-ориентированного фреймворка, чем просто предоставление меньшего числа API.
Привязка — это стоимость выхода¶
Полезный способ оценить привязку — спросить:
Какова стоимость сказать «нет» значению по умолчанию фреймворка?
Если ответ:
это сильная привязка.
Если ответ:
это сильная привязка.
Если ответ:
это хрупкая привязка.
Если ответ:
это относительно слабая привязка.
Если ответ:
это ещё слабее.
Модульный дизайн Kora улучшает эту стоимость выхода.
Фиче фреймворка, которая никогда не подключена к графу приложения, не нужно отключать.
Компонент по умолчанию, который можно переопределить, не нужно патчить.
Отсутствующую интеграцию можно добавить через @Module.
Это не означает, что Kora создаёт нулевую привязку. Любой фреймворк вводит конвенции, аннотации, сгенерированный код, формы конфигурации и инструменты сборки, которые создают стоимость переключения.
Значимое утверждение уже:
Kora пытается предотвратить превращение своих значений по умолчанию в обязательные архитектурные зависимости, когда обычная композиция может оставаться открытой.
Это более защитимое и полезное определение.
NIH — не о том, реализует ли фреймворк что-то¶
Критика «Not Invented Here» часто применяется слишком широко.
Если фреймворк предоставляет собственную абстракцию репозитория, HTTP-аннотации, слой отказоустойчивости, интеграцию планировщика или модель конфигурации, кто-то может сказать:
Зачем вообще это реализовывать? Другая библиотека уже существует.
Но само существование кода, принадлежащего фреймворку, — не реальная опасность.
Реальный риск NIH появляется, когда фреймворк говорит:
Это создаёт закрытый мир.
Более здоровая модель:
с:
Риск NIH поэтому не в том, что фреймворк реализует функциональность сам.
Риск появляется, когда эта реализация становится обязательной, непрозрачной или дорогой для замены.
Модульный и графовый дизайн Kora явно пытается избежать этой ловушки.
Нативные библиотеки остаются опцией¶
Это становится особенно важным при интеграции сторонних технологий.
Фреймворку не нужна официальная обёртка для каждой библиотеки, если нативный клиент может стать обычным компонентом графа.
Предположим, сервису нужна библиотека, которую Kora официально не поддерживает.
Путь расширения может быть:
нативная библиотека
↓
конфигурация
↓
фабрика @Module
↓
необязательный жизненный цикл / телеметрия
↓
граф приложения
Нет требования, чтобы библиотека сначала стала специфичной для Kora абстракцией.
Это ещё один способ, которым сильные значения по умолчанию избегают превращения в привязку.
Фреймворк может предоставлять предпочтительные интеграции для общих технологий, оставляя остальную JVM-экосистему доступной.
Вы можете расширять вертикально или горизонтально¶
Модель композиции Kora поддерживает два широких вида расширения.
Вертикальное расширение¶
Замените или настройте часть существующей интеграции.
Примеры могут включать:
- кастомного провайдера тегов телеметрии;
- кастомный маппер;
- кастомный перехватчик;
- кастомный обработчик безопасности;
- кастомный сериализатор;
- альтернативную фабрику клиента.
Окружающий модуль остаётся.
Меняется только одна политика или компонент.
Форма:
Горизонтальное расширение¶
Добавьте совершенно новую возможность.
Например:
кастомные фича-флаги
кастомный SDK вендора
кастомный внутренний RPC-клиент
кастомный движок рабочих процессов
кастомная система аудита
Форма:
Здоровый фреймворк должен поддерживать оба.
Вертикальное расширение предотвращает превращение поведения по умолчанию в жёсткое.
Горизонтальное расширение предотвращает превращение официального каталога модулей в границу экосистемы.
Явные графы делают замену понятной¶
Авто-обнаружение времени выполнения может делать замену удивительно трудной.
Если фреймворк создаёт объект потому, что:
- класс присутствует;
- свойство имеет значение;
- другой бин отсутствует;
- была обнаружена аннотация;
- совпала условная конфигурация;
- правило порядка выбрало одного кандидата;
тогда замена объекта может потребовать понимания всего процесса обнаружения.
Явный граф снижает число скрытых условий.
Композиция приложения говорит вам, какие модули участвуют.
Контракты говорят вам, какие зависимости требуются.
Компилятор говорит вам, если граф невалиден.
Это особенно ценно во время настройки.
Вопрос:
Почему моя замена не победила?
в идеале должен быть отвечаем из отношений типов и правил графа, а не из скрытого состояния времени выполнения.
Валидация на этапе компиляции делает расширяемость безопаснее¶
Расширяемость часто увеличивает риск, потому что становится возможным больше комбинаций.
Типичный обмен:
Валидация на этапе компиляции меняет этот обмен.
Если кастомные компоненты используют ту же модель графа, фреймворк может валидировать многие кастомные композиции до запуска приложения.
Это позволяет Kora поддерживать открытый граф, не отказываясь от сильных структурных гарантий.
Например, кастомный модуль может создавать:
которому требуется:
Если одна из этих зависимостей недоступна, граф должен падать во время компиляции, а не после развёртывания.
Расширяемость остаётся явной и типизированной.
Это гораздо лучшая модель, чем динамическая загрузка произвольных плагинов и обнаружение несовместимостей при запуске.
Единый граф предотвращает «острова расширений»¶
Некоторые фреймворки накапливают подсистемы, которые фактически ведут себя как отдельные контейнеры.
Основная система DI управляет сервисами приложения.
Подсистема безопасности управляет собственными провайдерами.
Фреймворк персистентности управляет репозиториями.
Библиотека обмена сообщениями имеет реестр.
Фреймворк расширений управляет плагинами.
Планировщик имеет другой жизненный цикл.
Приложение становится федерацией скрытых графов объектов.
Это может работать, но усложняет рассуждение.
Архитектурный идеал Kora — чтобы эти заботы, насколько практично, вливались в один граф приложения.
Это важно, потому что зависимости могут естественно пересекать границы интеграций.
Кастомный компонент может зависеть от телеметрии фреймворка.
Компонент фреймворка может зависеть от конфигурации приложения.
Сгенерированный компонент может зависеть от кастомной реализации.
Граф остаётся одной системой.
Это практическое значение фразы:
Кастомные и встроенные компоненты участвуют в одной модели.
Тестирование раскрывает, реальна ли расширяемость¶
Поддержка тестирования фреймворка — хорошее место, чтобы проверить, действительно ли кастомные компоненты первоклассны.
Тестовый граф Kora может заменять компоненты, добавлять компоненты и сохранять реальные зависимости графа вокруг них.
Это означает, что тест может взять:
и заменить:
моком, позволяя Kora собрать остальную часть требуемого графа.
Тот же принцип обобщается за пределы моков.
Тестирование демонстрирует, что компоненты графа — заменяемые единицы, а не жёстко закодированные объекты времени выполнения.
Фреймворк, который делает компоненты лёгкими для замены в тестах, часто имеет более здоровую модель инверсии управления и в продакшене.
Замена отличается от мутации¶
Есть ещё одно тонкое, но важное различие дизайна.
Фреймворк может поддерживать настройку, позволяя пользователям мутировать существующий внутренний объект:
или позволяя приложению предоставлять замену:
Мутация может быть удобна для мелких правок.
Замена обычно архитектурно чище, потому что владение явно.
Приложение знает, какой объект оно создало.
Фреймворк знает только контракт.
Меньше скрытого состояния и меньше допущений о порядке.
Модель компонентов Kora естественно поддерживает дизайн, ориентированный на замену.
Расширяемость без нескольких моделей программирования¶
Ещё один способ, которым фреймворки добиваются гибкости, — одновременная поддержка нескольких парадигм программирования.
Например:
Это создаёт гибкость, но также резко расширяет поверхность фреймворка.
Каждой крупной интеграции могут понадобиться несколько вариантов.
Телеметрия должна работать во всех них.
Распространение контекста различается.
Транзакции различаются.
Тестирование различается.
Документация ветвится.
Kora 2 намеренно сужает это, возвращаясь к обычным синхронным API Java и Kotlin на виртуальных потоках для кода приложения.
Это полезный пример различия между широтой модели программирования и архитектурной расширяемостью.
Kora может поддерживать более узкую модель программирования, оставаясь открытым для кастомных компонентов.
Фреймворку не нужны три способа выразить каждый метод сервиса, чтобы быть расширяемым.
Ему нужна публичная модель композиции, которая принимает разные реализации.
Расширяемость должна снижать давление форка¶
Один из сильнейших индикаторов закрытого фреймворка — как часто серьёзным пользователям приходится его форкать.
Форки происходят по многим причинам, и некоторые неизбежны. Но частые форки часто указывают на то, что критические политики встроены во внутренности фреймворка.
Лучшая модель расширения позволяет:
вместо:
Форкинг имеет высокую долгосрочную стоимость.
Теперь вы владеете:
- конфликтами слияния вышестоящих;
- исправлениями безопасности;
- синхронизацией релизов;
- тестированием совместимости;
- публикацией сборок;
- внутренней документацией;
- ответственностью за миграцию.
Маленький кастомный модуль радикально дешевле.
Вот почему заменяемость — не академическое свойство. Она напрямую влияет на экономику сопровождения.
Лучшая точка расширения часто — обычный интерфейс¶
Фреймворки иногда создают изощрённые SPI для возможностей, которые Java-интерфейсы уже могли бы выразить.
Есть валидные случаи для формального SPI, особенно когда требуются обнаружение, версионирование, изоляция или загрузка плагинов.
Но внутри одного приложения публичный интерфейс плюс внедрение зависимостей часто достаточно.
Например:
Реализация по умолчанию может существовать.
Приложение может предоставить другую реализацию.
Потребитель зависит от TenantResolver.
Больше ничего не требуется.
Это полезный принцип дизайна:
Не изобретайте систему расширения фреймворка, когда обычная инверсия зависимостей уже решает проблему.
Граф-ориентированная архитектура Kora выигрывает от этого принципа.
Расширение Kora кастомной интеграцией¶
Рассмотрим сервис, которому нужно внутреннее хранилище документов, недоступное как официальная интеграция Kora.
Нативный клиент может выглядеть так:
Кастомный контракт конфигурации может представлять требуемые настройки:
Кастомный модуль может сконструировать клиент:
Теперь приложение может его инжектировать:
Если клиент владеет ресурсами, добавьте поведение жизненного цикла.
Если ему нужна телеметрия, подключите телеметрию во время построения.
Если важна доступность, добавьте пробу готовности.
Если несколько сервисов его используют, выделите модуль в общую библиотеку.
Ни в один момент интеграции не нужно обходить Kora.
Это практический тест расширяемости.
Кастомный модуль может стать частью вашей платформы¶
Это становится особенно ценным для крупных организаций.
У большинства предприятий есть инфраструктура, которую никогда не поставит ни один публичный фреймворк:
внутренний auth
внутреннее обнаружение сервисов
внутреннее хранилище секретов
внутренний RPC
внутренний аудит
внутренние фича-флаги
внутренние метаданные развёртывания
внутренний сервис конфигурации
Фреймворк, который предполагает, что все полезные интеграции должны приходить из его официальной экосистемы, плохо подходит для этих сред.
Модульная модель Kora позволяет платформенной команде создавать внутренние модули, используя ту же механику графа.
Например:
company-kora-auth
company-kora-observability
company-kora-feature-flags
company-kora-audit
company-kora-service-discovery
Команды приложений затем компонуют их ровно так, как компонуют модули фреймворка.
Платформа организации становится расширением Kora без необходимости приватного форка Kora.
Это одно из сильнейших реальных преимуществ простой модели модулей.
Сильные значения по умолчанию улучшают качество расширений¶
Здесь есть интересный парадокс.
Фреймворк с меньшим числом встроенных выборов иногда может делать кастомные расширения легче для проектирования.
Почему?
Потому что автору расширения нужно интегрироваться только с одной моделью фреймворка.
Если фреймворк поддерживает:
три HTTP-модели
два режима DI
две системы телеметрии
блокирующие и реактивные репозитории
несколько систем конфигурации
переиспользуемому расширению может понадобиться решить, какие комбинации оно поддерживает.
Если Kora предоставляет одну связную модель, кастомный модуль имеет более ясную цель.
Вопрос интеграции становится:
а не:
Сильные значения по умолчанию снижают размерность расширения.
Стоимость расширяемости часто комбинаторна¶
Предположим, фреймворк предлагает:
- 3 HTTP-модели;
- 2 модели персистентности;
- 3 асинхронные модели;
- 2 API телеметрии.
Это не создаёт двенадцать выборов.
Это создаёт потенциально десятки взаимодействий.
Мейнтейнеру фреймворка приходится рассуждать о совместимости.
Автору расширения приходится решать, какие комбинации важны.
Пользователям приходится отлаживать комбинации, которые могут быть не одинаково зрелыми.
Более узкий фреймворк может избежать большей части этого комбинаторного взрыва.
Это одна из причин, по которой «много точек расширения» не автоматически является признаком превосходной расширяемости.
Иногда это признак того, что у фреймворка много внутренних измерений, требующих точек расширения.
Хорошее значение по умолчанию снижает стоимость решений¶
Для большинства команд путь по умолчанию важен гораздо чаще, чем путь расширения.
Поэтому фреймворк должен оптимизировать оба:
Эти цели совместимы.
Общий путь не должен требовать комитета по дизайну.
Необычный путь не должен требовать форка фреймворка.
Философию Kora можно резюмировать ровно в этих терминах.
Фреймворк хочет быть мнение-ориентированным там, где повторяющиеся выборы создают мало ценности.
Он хочет оставаться компонуемым там, где реальные требования различаются.
Приложение владеет финальной композицией¶
Одно из самых важных следствий явного корня композиции @KoraApp в том, что приложение — а не фреймворк — описывает финальный граф.
Это означает, что архитектура приложения видна на верхнем уровне.
Корень может компоновать:
Это ценно, потому что приложение может ответить:
Какие возможности фреймворка мы выбрали?
Какие внутренние интеграции участвуют?
Какие модули наши?
Где граница композиции?
Это гораздо яснее, чем система, где classpath неявно определяет архитектуру приложения.
Явная композиция — поэтому и механизм расширяемости, и механизм управления.
Всё ещё существует такая вещь, как плохой дизайн расширения¶
Открытый граф Kora не автоматически делает каждую настройку мудрой.
Команда всё ещё может создать:
- слишком много обёрток;
- конфликтующие реализации;
- чрезмерно широкие общие модули;
- глубокие циклы зависимостей;
- внутренние абстракции, дублирующие вышестоящие библиотеки;
- кастомные замены, ломающие ожидаемую семантику телеметрии или жизненного цикла.
Расширяемость создаёт ответственность.
Правильный урок не таков:
Всё должно быть заменено.
Он таков:
Замена должна быть возможна, когда требования её оправдывают.
Значения по умолчанию существуют, потому что кодируют полезные решения.
Их замена должна быть вызвана ясной потребностью, а не рефлексом настраивать.
Когда значение по умолчанию обычно должно побеждать¶
Встроенная реализация обычно правильный выбор, когда:
- она удовлетворяет функциональным требованиям;
- её производительность достаточна;
- она следует желаемой модели наблюдаемости;
- у неё корректное поведение жизненного цикла;
- она активно сопровождается;
- у команды нет значимого дифференцирующего требования.
Замена работающего значения по умолчанию создаёт владение.
Теперь команда владеет:
- поведением реализации;
- обновлениями;
- тестированием;
- граничными случаями;
- документацией.
Расширяемость ценна, потому что даёт опцию.
Это не означает, что каждая опция должна использоваться.
Когда замена оправдана¶
Кастомная реализация становится разумной, когда есть явное несоответствие.
Например:
- специфичная для организации политика безопасности;
- кастомные правила кардинальности телеметрии;
- специфичное для вендора поведение клиента;
- внутренняя инфраструктура;
- требования производительности;
- совместимость с легаси-системой;
- миграция между технологиями;
- регуляторное поведение;
- кастомная сериализация;
- специализированная политика сбоев.
Важный момент в том, что Kora не требует, чтобы эти требования становились патчами фреймворка.
Они могут оставаться кодом приложения или платформы.
Граф — лучшая граница расширения, чем глобальная конфигурация¶
Ещё одна распространённая техника настройки — глобальная конфигурация.
Фреймворк может раскрывать сотни свойств:
Это может делать фреймворк настраиваемым, не делая его по-настоящему расширяемым.
Конфигурация позволяет только те выборы, которые авторы фреймворка предвидели.
Композиция позволяет поведение, определяемое приложением.
Это различие фундаментально.
Конфигурация говорит:
Выбирай среди моих опций.
Внедрение зависимостей говорит:
Предоставь реализацию этого контракта.
Второе более открыто.
Kora широко использует конфигурацию там, где конфигурация уместна, но граф предоставляет более сильный выход из предопределённых опций.
Явная композиция помогает миграции¶
Заменяемость становится особенно ценной во время миграций.
Предположим, организация переходит с одной реализации клиента на другую.
Жёсткий фреймворк поощряет «день флага»:
Явный граф может поддерживать поэтапный подход.
Например:
или:
Приложение может временно запускать оба.
Трафик можно переключать.
Результаты можно сравнивать.
Старая реализация может в конце концов исчезнуть.
Это практическая форма расширяемости: архитектура может представлять переходные состояния.
Реальным системам эти состояния нужны часто.
Явные теги помогают нескольким реализациям сосуществовать¶
Теги Kora предоставляют ещё один механизм контролируемой настройки.
Иногда приложению легитимно нужно несколько компонентов одного базового типа.
Например:
основная база данных
аналитическая база данных
внутренний HTTP-клиент
внешний HTTP-клиент
старый провайдер
новый провайдер
Система DI, которая предполагает один глобальный компонент на тип, может делать такие дизайны неудобными.
Тегированные зависимости позволяют графу явно различать реализации.
Это ценно во время:
- миграций;
- мульти-тенантной архитектуры;
- мульти-региональных интеграций;
- специализированных клиентов;
- тестирования.
Более широкий урок в том, что сильные значения по умолчанию не требуют притворяться, что концепции может существовать только один экземпляр.
Они требуют, чтобы общий случай был простым, а продвинутые случаи оставались представимыми.
Расширяемость на этапе компиляции отличается от загрузки плагинов во время выполнения¶
Kora — не в первую очередь платформа плагинов времени выполнения.
Это важное различие.
Если приложение должно динамически загружать неизвестные плагины после компиляции, граф приложения на этапе компиляции — не тот же инструмент, что реестр плагинов времени выполнения.
Но большинство расширяемости бэкенд-сервисов не требует неизвестных плагинов времени выполнения.
Оно требует:
- специфичных для приложения реализаций;
- внутренних модулей;
- альтернативных клиентов;
- адаптеров;
- кастомной телеметрии;
- переопределений политик.
Они известны, когда приложение строится.
Композиция на этапе компиляции чрезвычайно хорошо подходит для этого класса расширений, потому что держит получившийся граф валидированным и явным.
Фреймворк не следует критиковать за отсутствие динамической загрузки плагинов, когда случай использования её не требует.
Расширяемость следует оценивать против реальных сценариев изменений¶
Лучший способ проверить расширяемость фреймворка — не считать SPI.
Задавайте конкретные вопросы.
Можем ли мы заменить политику тегов метрик по умолчанию?
Можем ли мы интегрировать внутренний клиент?
Можем ли мы использовать другую реализацию контракта?
Можем ли мы добавить компонент с требованиями жизненного цикла?
Может ли кастомный модуль зависеть от встроенной телеметрии?
Могут ли встроенные компоненты зависеть от нашей реализации?
Можем ли мы тестировать граф с заменяющим компонентом?
Можем ли мы избегать подтягивания модулей, которые нам не нужны?
Можем ли мы мигрировать между двумя реализациями без переписывания бизнес-логики?
Можем ли мы выделить нашу кастомную интеграцию в переиспользуемую библиотеку?
Если ответ «да» через обычную композицию, фреймворк значимо расширяем.
Сильные значения по умолчанию и расширяемость усиливают друг друга¶
Может казаться, что мнение-ориентированные значения по умолчанию и расширяемость тянут в противоположные стороны.
В хорошо спроектированном фреймворке они усиливают друг друга.
Сильные значения по умолчанию создают:
согласованные кодовые базы
предсказуемое введение в проект
более простую документацию
общие конвенции
более низкую когнитивную нагрузку
Расширяемость создаёт:
адаптируемость
пути миграции
специфичные для организации интеграции
выход из неподходящих значений по умолчанию
долгосрочную жизнеспособность
Лучшая архитектура фреймворка сочетает их:
┌─────────────────────┐
│ сильное значение │
│ по умолчанию │
│ один ясный путь │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ публичный контракт │
│ явный граф │
└─────────┬───────────┘
│
┌───────────┴───────────┐
▼ ▼
реализация по умолчанию кастомная реализация
│ │
└───────────┬───────────┘
▼
граф приложения
Значение по умолчанию снижает ежедневную стоимость решений.
Публичный контракт защищает приложение от привязки.
Почему это важнее по мере старения фреймворка¶
Молодой фреймворк часто может делать большие изменения дизайна.
Зрелый фреймворк накапливает обязательства совместимости.
Если значения по умолчанию глубоко встроены во внутренности, каждое будущее изменение становится труднее.
Если значения по умолчанию сидят за заменяемыми контрактами, у фреймворка больше места для эволюции.
Приложения могут продолжать использовать старое поведение через кастомную реализацию.
Организации могут мигрировать постепенно.
Экспериментальные интеграции могут жить вне ядра фреймворка, пока не докажут свою полезность.
Это создаёт более здоровую долгосрочную эволюцию.
Расширяемый граф — поэтому не только фича для пользователей.
Это механизм устойчивости для самого фреймворка.
Реальная мера расширяемости¶
Полезное уравнение:
Лучшее приближение:
По этой мере у Kora сильная история.
Модули явны.
Компоненты типизированы.
Кастомные фабрики обычны.
Значения по умолчанию можно моделировать как заменяемые компоненты.
Сгенерированный код может удовлетворять публичные контракты.
Кастомные интеграции могут использовать тот же граф.
Компилятор всё ещё валидирует результат.
Тесты могут заменять части графа.
Жизненный цикл и телеметрия остаются доступными.
Приложение остаётся одной системой.
Заключение¶
Сильное значение по умолчанию — это не привязка.
Привязка начинается, когда значение по умолчанию становится неотделимым от архитектуры фреймворка.
Мнение-ориентированный фреймворк может оставаться открытым, если значение по умолчанию — просто рекомендованная реализация публичного контракта, если модули опциональны, если кастомные компоненты участвуют в том же графе зависимостей и если приложения могут заменять или расширять поведение, не пересекая приватные внутренности фреймворка.
Это важное различие в Kora.
Kora намеренно пытается предоставить один ясный путь для общих бэкенд-проблем. Это снижает усталость от решений, держит команды согласованными, упрощает документацию и избегает экосистемы фреймворка, где несколько конкурирующих моделей программирования решают одну и ту же проблему слегка разными способами.
Но «один ясный путь» не означает «одна реализация навсегда».
Граф приложения остаётся явным и компонуемым.
Встроенные модули входят, потому что приложение их выбирает.
Кастомные модули могут входить через ту же модель.
Сторонний клиент может стать обычным компонентом.
Реализация по умолчанию может уступить другой реализации того же контракта.
Сгенерированный код может оставаться за обычными интерфейсами.
Кастомные компоненты могут участвовать в жизненном цикле, телеметрии, тестировании и валидации зависимостей.
Приложению не нужно форкать Kora просто потому, что одно требование отличается от значения по умолчанию фреймворка.
Вот почему большой каталог SPI — не лучшая мера расширяемости.
Фреймворк может иметь десятки хуков расширения и всё равно относиться к расширениям как к второсортному коду.
Более сильная модель противоположна:
Фреймворк по-настоящему расширяем не потому, что у него много точек расширения. Он расширяем, когда кастомный код участвует в той же модели, что и встроенный код.
Архитектура Kora построена вокруг этой идеи.
Его значения по умолчанию существуют, чтобы сделать общий путь очевидным, а не закрыть необычный путь.
Его система модулей существует, чтобы сервисы могли начинаться малыми и добавлять только те возможности, которые им действительно нужны.
Его граф зависимостей существует, чтобы дать встроенным и кастомным компонентам один язык композиции.
Его валидация на этапе компиляции существует, чтобы держать настройку безопасной, а не толкать расширения в непроверяемый запасной выход времени выполнения.
И его использование публичных контрактов и обычной композиции Java/Kotlin означает, что команды могут расширять фреймворк, не становясь сначала экспертами во внутренностях фреймворка.
Тот же взгляд даёт более точный ответ на критику NIH.
Риск «Not Invented Here» не просто в том, что фреймворк предоставляет собственную реализацию чего-то, что можно было получить в другом месте. Реализации, принадлежащие фреймворку, могут быть полезны, когда они дают лучшую интеграцию, валидацию на этапе компиляции, наблюдаемость, производительность или более ясную модель программирования.
Реальный риск начинается, когда реализация, принадлежащая фреймворку, становится обязательной, непрозрачной или трудной для замены.
Это ловушка, которую модель приложения Kora спроектирована избегать.
Хорошее значение по умолчанию должно делать общий случай проще.
Хорошая модель расширения должна делать необычный случай возможным.
Сильный фреймворк должен делать и то и другое.
Философию Kora можно поэтому резюмировать одним предложением:
Используйте значение по умолчанию, когда оно подходит, заменяйте его, когда не подходит, и держите оба внутри одного явного, валидируемого на этапе компиляции графа приложения.