при работе с Dart и Flutter становится очевидным: многие DI-библиотеки либо слишком тяжёлые, либо слишком простые. одни предлагают автоматическую магию, скрытые зависимости, runtime-рефлексию - что усложняет тестирование и снижает производительность. другие дают лишь базовый функционал, который не покрывает типичные сценарии: scoped-контейнеры, несколько реализаций одного интерфейса, декораторы, модули
в такой ситуации мне понадобился DI-контейнер, отвечающий следующим требованиям:
-
минимальный API, без магии
-
высокая производительность
-
явно управляемый жизненный цикл зависимостей
-
поддержка real-world сценариев (scoped-контейнеры, декораторы, ключевые реализации, модули)
на основе этих требований я разработал DRTDI
задачи, которые должен был решать DRTDI
при проектировании DRTDI я сразу сформулировал основные задачи:
-
прозрачность - контейнер не должен делать ничего, на что пользователь не дал явного указания. Никакой мета-магии, автоматического сканирования или кодогенерации
-
производительность - разрешение зависимостей должно быть быстрым, без рефлексии, proxy-классов, dynamic code
-
гибкость - контейнер должен поддерживать:
-
несколько жизненных циклов (singleton, transient, scoped)
-
иерархию контейнеров (parent/child)
-
регистрацию нескольких реализаций одного интерфейса с ключами (keyed registrations)
-
возможность применения декораторов к зарегистрированным сервисам
-
группировку регистраций в модули для структурирования архитектуры
-
-
универсальность - работа как в обычных Dart-проектах, так и в приложениях на Flutter
основные концепции DRTDI
жизненные циклы (Lifetimes)
-
singleton - единый объект на весь жизненный цикл контейнера
-
transient - новый экземпляр при каждом разрешении
-
scoped - жизненный цикл ограничен scope-контейнером; при создании дочернего контейнера - создаётся новый экземпляр
благодаря этому можно гибко управлять временем жизни объектов и их изоляцией
иерархия контейнеров
контейнеры могут быть организованы в дерево: есть корневой контейнер, дочерние, и т. д
это даёт возможность разделять зависимости для разных модулей, экранов или тестовых окружений, избегая глобального состояния и обеспечивая изоляцию
keyed registrations — несколько реализаций одного интерфейса
поддержка ключей (например, строковых или enum) при регистрации позволяет иметь несколько реализаций одного интерфейса, например:
container.register<Storage>((c) => FileStorage(), key: 'file');
container.register<Storage>((c) => CloudStorage(), key: 'cloud');
при разрешении можно явно указать, какая реализация нужна:
final fileStorage = container.resolve<Storage>(key: 'file');
final cloudStorage = container.resolve<Storage>(key: 'cloud');
это особенно полезно, если в приложении требуется переключение реализаций, например, для тестов, разных окружений или конфигураций пользователя.
декораторы (Decorators / Middlewares)
после создания объекта контейнер может "обернуть" его в дополнительный слой - например, для логирования, кэширования, тайминга, проверки прав доступа и т. д.:
container.decorate<ApiClient>((original) {
return LoggingApiClient(original);
});
благодаря этому можно легко расширять поведение сервисов без изменения их исходной реализации
модули (модульная регистрация зависимостей)
DRTDI позволяет группировать регистрации по логическим модулям, что делает конфигурацию DI чистой и поддерживаемой. например, модуль для сетевого слоя, модуль для хранения данных, модуль для бизнес-логики и т. п
как реализовано внутри - уход от магии, ставка на простоту и скорость
при реализации DRTDI я сознательно отказался от:
-
runtime-рефлексии
-
автоматического сканирования классов
-
code-generation
-
dynamic proxy / runtime-сборки
всё, что делает контейнер - это хранит маппинг типов (и ключей) → фабрик, и при запросе выполняет соответствующую фабрику, возвращая объект. при необходимости - применяет декораторы или возвращает ранее созданный singleton
такой подход даёт:
-
детерминированность: поведение контейнера полностью предсказуемо
-
минимальные накладные расходы: нет overhead от рефлексии или кодогенерации
-
простоту понимания / отладки: можно легко отследить, где и что создаётся
-
гибкость: расширения (декораторы, ключи, scoped) легко реализуются без сложной инфраструктуры
использование: примеры кода
регистрация и разрешение простых сервисов
final container = Container();
// регистрация singleton
container.registerSingleton<Config>(() => Config());
// регистрация transient
container.registerTransient<UserService>(() => UserService(
container.resolve<Config>(),
));
// разрешение
var config = container.resolve<Config>();
var userService = container.resolve<UserService>();
scoped-контейнеры
final root = Container();
root.registerSingleton<GlobalService>(() => GlobalService());
final screenScope = root.createChild();
screenScope.registerTransient<ScreenService>(() => ScreenService(root.resolve<GlobalService>())
);
var screenService = screenScope.resolve<ScreenService>();
keyed реализации
root.register<Storage>(() => FileStorage(), key: 'file');
root.register<Storage>(() => CloudStorage(), key: 'cloud');
var fileStorage = root.resolve<Storage>(key: 'file');
var cloudStorage = root.resolve<Storage>(key: 'cloud');
декораторы
root.register<ApiClient>(() => ApiClientImpl());
root.decorate<ApiClient>((client) {
return LoggingApiClient(client);
});
var api = root.resolve<ApiClient>();
// api — это LoggingApiClient, оборачивающий ApiClientImpl
почему DRTDI может быть полезен на практике
-
минимальный overhead - контейнер не добавляет лишних абстракций, runtime-прокси или магии
-
предсказуемость поведения - всё делается явно, видно, какие зависимости зарегистрированы, как они создаются и когда
-
гибкость для разных задач - singleton, scoped, transient; ключевые реализации; декораторы; модули
-
лёгкость сопровождения и понимания архитектуры - конфигурация DI остаётся прозрачной, лёгкой для чтения и рефакторинга
-
универсальность - библиотека пригодна как для небольших утилит (CLI), так и для крупных Flutter-приложений
почему проект открыт - и как можно использовать DRTDI
DRTDI - это результат моего личного опыта, моих жалоб и моих предпочтений в архитектуре. когда я убедился, что контейнер решает реальные задачи и при этом не накладывает лишнюю нагрузку, я сделал его открытым
исходники доступны в публичном репозитории:
https://github.com/C0dwiz/drtdi
цель - дать разработчикам инструмент, который:
-
остаётся лёгким
-
даёт контроль
-
не навязывает сложную инфраструктуру
-
остаётся быстрым и предсказуемым
если вам нужен DI-контейнер, который не усложняет проект, а служит надёжной и простой основой для зависимостей - возможно, DRTDI вам подойдёт
заключение
DRTDI - это попытка предложить DI без компромиссов: минимальный API, высокая производительность, гибкая архитектура
если вы цените:
-
прозрачность зависимостей и конфигурации
-
контроль над жизненным циклом объектов
-
простоту и лёгкость сопровождения
-
производительность и отсутствие лишних runtime-зависимостей
то DRTDI - достойный вариант
я открыт к вопросам, предложениям, PR и обсуждениям. возможно, вместе удастся сделать DI в экосистеме Dart чуть удобнее и надёжнее
Автор: CodWiz
