refactor: unify DIAdapter with generics, ensure type-safety & scalability in benchmark_di

- Refactor DIAdapter to generic abstract class; align interfaces for Cherrypick, GetIt, Riverpod.
- Remove all dynamic/object usage from dependency registration and bench scenarios.
- Universal getUniversalRegistration function now fully type-safe for all DIs.
- Fix teardown and lifecycle for RiverpodAdapter to prevent disposed Container errors.
- Update CLI and benchmark entry points; validated all scenarios and stress modes for each DI adapter.
This commit is contained in:
Sergey Penkovsky
2025-08-07 13:44:39 +03:00
parent 590b876cf4
commit 54446868e4
7 changed files with 193 additions and 192 deletions

View File

@@ -30,29 +30,69 @@ class BenchmarkCliRunner {
for (final c in config.chainCounts) {
for (final d in config.nestDepths) {
BenchmarkResult benchResult;
final di = config.di == 'getit'
? GetItAdapter()
: config.di == 'riverpod'
? RiverpodAdapter()
: CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
if (config.di == 'getit') {
final di = GetItAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
} else if (config.di == 'riverpod') {
final di = RiverpodAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
} else {
final benchSync = UniversalChainBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
final di = CherrypickDIAdapter();
if (scenario == UniversalScenario.asyncChain) {
final benchAsync = UniversalChainAsyncBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode,
);
benchResult = await BenchmarkRunner.runAsync(
benchmark: benchAsync,
warmups: config.warmups,
repeats: config.repeats,
);
} else {
final benchSync = UniversalChainBenchmark(di,
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario,
);
benchResult = await BenchmarkRunner.runSync(
benchmark: benchSync,
warmups: config.warmups,
repeats: config.repeats,
);
}
}
final timings = benchResult.timings;
timings.sort();

View File

@@ -1,13 +1,18 @@
import 'package:cherrypick/cherrypick.dart';
import 'di_adapter.dart';
/// DIAdapter implementation for the CherryPick DI library using registration callbacks.
class CherrypickDIAdapter implements DIAdapter {
/// Универсальный DIAdapter для CherryPick с поддержкой subScope без дублирования логики.
class CherrypickDIAdapter extends DIAdapter<Scope> {
Scope? _scope;
final bool _isSubScope;
CherrypickDIAdapter([Scope? scope, this._isSubScope = false]) {
_scope = scope;
}
@override
void setupDependencies(void Function(dynamic container) registration) {
_scope = CherryPick.openRootScope();
void setupDependencies(void Function(Scope container) registration) {
_scope ??= CherryPick.openRootScope();
registration(_scope!);
}
@@ -21,45 +26,18 @@ class CherrypickDIAdapter implements DIAdapter {
@override
void teardown() {
CherryPick.closeRootScope();
_scope = null;
if (!_isSubScope) {
CherryPick.closeRootScope();
_scope = null;
}
// SubScope teardown не требуется
}
@override
CherrypickDIAdapter openSubScope(String name) {
final sub = _scope!.openSubScope(name);
return _CherrypickSubScopeAdapter(sub);
return CherrypickDIAdapter(_scope!.openSubScope(name), true);
}
@override
Future<void> waitForAsyncReady() async {}
}
/// Internal adapter for a CherryPick sub-scope (callbacks based).
class _CherrypickSubScopeAdapter extends CherrypickDIAdapter {
final Scope _subScope;
_CherrypickSubScopeAdapter(this._subScope);
@override
void setupDependencies(void Function(dynamic container) registration) {
registration(_subScope);
}
@override
T resolve<T extends Object>({String? named}) =>
named == null ? _subScope.resolve<T>() : _subScope.resolve<T>(named: named);
@override
Future<T> resolveAsync<T extends Object>({String? named}) async =>
named == null ? await _subScope.resolveAsync<T>() : await _subScope.resolveAsync<T>(named: named);
@override
void teardown() {
// subScope teardown не требуется
}
@override
CherrypickDIAdapter openSubScope(String name) {
return _CherrypickSubScopeAdapter(_subScope.openSubScope(name));
}
}

View File

@@ -1,24 +1,21 @@
/// Абстракция для DI-адаптера с использованием функций регистрации.
///
/// Позволяет использовать любые DI-контейнеры: и модульные, и безмодульные.
abstract class DIAdapter {
/// Устанавливает зависимости с помощью одной функции регистрации.
///
/// Функция принимает выбранный DI-контейнер, задаваемый реализацией.
void setupDependencies(void Function(dynamic container) registration);
/// Универсальная абстракция для DI-адаптера с унифицированной функцией регистрации.
/// Теперь для каждого адаптера задаём строгий generic тип контейнера.
abstract class DIAdapter<TContainer> {
/// Устанавливает зависимости с помощью строго типизированного контейнера.
void setupDependencies(void Function(TContainer container) registration);
/// Резолвит (возвращает) экземпляр типа [T] (по имени, если требуется).
T resolve<T extends Object>({String? named});
/// Асинхронно резолвит экземпляр типа [T].
/// Асинхронно резолвит экземпляр типа [T] (если нужно).
Future<T> resolveAsync<T extends Object>({String? named});
/// Уничтожает/отчищает DI-контейнер.
void teardown();
/// Открывает дочерний под-scope (если применимо).
DIAdapter openSubScope(String name);
/// Открывает дочерний scope и возвращает новый адаптер (если поддерживается).
DIAdapter<TContainer> openSubScope(String name);
/// Ожидание готовности DI контейнера (нужно для async DI, например get_it)
Future<void> waitForAsyncReady();
/// Ожидание готовности DI контейнера (если нужно для async DI).
Future<void> waitForAsyncReady() async {}
}

View File

@@ -1,74 +1,59 @@
import 'package:get_it/get_it.dart';
import 'di_adapter.dart';
class GetItAdapter implements DIAdapter {
/// Универсальный DIAdapter для GetIt c поддержкой scopes и строгой типизацией.
class GetItAdapter extends DIAdapter<GetIt> {
late GetIt _getIt;
@override
void setupDependencies(void Function(dynamic container) registration) {
_getIt = GetIt.asNewInstance();
registration(_getIt);
}
@override
T resolve<T extends Object>({String? named}) => _getIt<T>(instanceName: named);
@override
Future<T> resolveAsync<T extends Object>({String? named}) async => _getIt<T>(instanceName: named);
@override
void teardown() => _getIt.reset();
@override
DIAdapter openSubScope(String name) {
// Открываем новый scope и возвращаем адаптер, который в setupDependencies будет использовать init.
return _GetItScopeAdapter(_getIt, name);
}
@override
Future<void> waitForAsyncReady() async {
await _getIt.allReady();
}
}
class _GetItScopeAdapter implements DIAdapter {
final GetIt _getIt;
final String _scopeName;
final String? _scopeName;
final bool _isSubScope;
bool _scopePushed = false;
void Function(dynamic container)? _pendingRegistration;
_GetItScopeAdapter(this._getIt, this._scopeName);
@override
void setupDependencies(void Function(dynamic container) registration) {
_pendingRegistration = registration;
// Создаём scope через pushNewScope с init для правильной регистрации
_getIt.pushNewScope(
scopeName: _scopeName,
init: (getIt) => _pendingRegistration?.call(getIt),
);
_scopePushed = true;
}
@override
T resolve<T extends Object>({String? named}) => _getIt<T>(instanceName: named);
@override
Future<T> resolveAsync<T extends Object>({String? named}) async => _getIt<T>(instanceName: named);
@override
void teardown() {
if (_scopePushed) {
_getIt.popScope();
_scopePushed = false;
/// Основной (root) и subScope-конструкторы.
GetItAdapter({GetIt? instance, String? scopeName, bool isSubScope = false})
: _scopeName = scopeName,
_isSubScope = isSubScope {
if (instance != null) {
_getIt = instance;
}
}
@override
DIAdapter openSubScope(String name) {
return _GetItScopeAdapter(_getIt, name);
void setupDependencies(void Function(GetIt container) registration) {
if (_isSubScope) {
// Создаём scope через pushNewScope с init
_getIt.pushNewScope(
scopeName: _scopeName,
init: (getIt) => registration(getIt),
);
_scopePushed = true;
} else {
_getIt = GetIt.asNewInstance();
registration(_getIt);
}
}
@override
T resolve<T extends Object>({String? named}) =>
_getIt<T>(instanceName: named);
@override
Future<T> resolveAsync<T extends Object>({String? named}) async =>
_getIt<T>(instanceName: named);
@override
void teardown() {
if (_isSubScope && _scopePushed) {
_getIt.popScope();
_scopePushed = false;
} else {
_getIt.reset();
}
}
@override
GetItAdapter openSubScope(String name) =>
GetItAdapter(instance: _getIt, scopeName: name, isSubScope: true);
@override
Future<void> waitForAsyncReady() async {
await _getIt.allReady();

View File

@@ -1,67 +1,71 @@
import 'package:riverpod/riverpod.dart';
import 'di_adapter.dart';
/// RiverpodAdapter реализует DIAdapter для универсального бенчмарка через Riverpod.
class RiverpodAdapter implements DIAdapter {
late ProviderContainer _container;
late final Map<String?, ProviderBase<Object?>> _namedProviders;
/// Унифицированный DIAdapter для Riverpod с поддержкой scopes и строгой типизацией.
class RiverpodAdapter extends DIAdapter<Map<String, ProviderBase<Object?>>> {
ProviderContainer? _container;
final Map<String, ProviderBase<Object?>> _namedProviders;
final ProviderContainer? _parent;
final bool _isSubScope;
// Основной конструктор
RiverpodAdapter() : _parent = null {
_namedProviders = <String?, ProviderBase<Object?>>{};
}
// Внутренний конструктор для дочерних скоупов
RiverpodAdapter._child(this._container, this._namedProviders, this._parent);
RiverpodAdapter({
ProviderContainer? container,
Map<String, ProviderBase<Object?>>? providers,
ProviderContainer? parent,
bool isSubScope = false,
}) : _container = container,
_namedProviders = providers ?? <String, ProviderBase<Object?>>{},
_parent = parent,
_isSubScope = isSubScope;
@override
void setupDependencies(void Function(dynamic container) registration) {
// Для главного контейнера
_container = _parent == null
void setupDependencies(void Function(Map<String, ProviderBase<Object?>> container) registration) {
_container ??= _parent == null
? ProviderContainer()
: ProviderContainer(parent: _parent);
registration(_namedProviders);
}
/// Регистрировать провайдеры нужно по имени-сервису.
/// Пример: container['SomeClass'] = Provider((ref) => SomeClass());
@override
T resolve<T extends Object>({String? named}) {
final provider = _namedProviders[named ?? T.toString()];
final key = named ?? T.toString();
final provider = _namedProviders[key];
if (provider == null) {
throw Exception('Provider not found for $named');
throw Exception('Provider not found for $key');
}
return _container.read(provider) as T;
return _container!.read(provider) as T;
}
@override
Future<T> resolveAsync<T extends Object>({String? named}) async {
final provider = _namedProviders[named ?? T.toString()];
final key = named ?? T.toString();
final provider = _namedProviders[key];
if (provider == null) {
throw Exception('Provider not found for $named');
throw Exception('Provider not found for $key');
}
// Если это FutureProvider — используем .future
if (provider.runtimeType.toString().contains('FutureProvider')) {
final result = await _container.read((provider as dynamic).future);
return result as T;
return await _container!.read((provider as dynamic).future) as T;
}
return resolve<T>(named: named);
}
@override
void teardown() {
_container.dispose();
_container?.dispose();
_container = null;
_namedProviders.clear();
}
@override
DIAdapter openSubScope(String name) {
// Создаём дочерний scope через новый контейнер с parent
final childContainer = ProviderContainer(parent: _container);
// Провайдеры будут унаследованы (immutable копия), но при желании можно их расширять в дочернем scope.
return RiverpodAdapter._child(childContainer, Map.of(_namedProviders), _container);
RiverpodAdapter openSubScope(String name) {
final newContainer = ProviderContainer(parent: _container);
return RiverpodAdapter(
container: newContainer,
providers: Map.of(_namedProviders),
parent: _container,
isSubScope: true,
);
}
@override

View File

@@ -6,10 +6,12 @@ import '../di_adapters/get_it_adapter.dart';
import 'universal_chain_module.dart';
import 'package:riverpod/riverpod.dart' as rp;
/// Возвращает универсальную функцию регистрации зависимостей,
/// подходящую под выбранный DI-адаптер.
void Function(dynamic) getUniversalRegistration(
DIAdapter adapter, {
/// Унифицированный generic-колбэк для регистрации зависимостей,
/// подходящий под выбранный DI-адаптер.
typedef Registration<TContainer> = void Function(TContainer);
Registration<TContainer> getUniversalRegistration<TContainer>(
DIAdapter<TContainer> adapter, {
required int chainCount,
required int nestingDepth,
required UniversalBindingMode bindingMode,
@@ -25,8 +27,9 @@ void Function(dynamic) getUniversalRegistration(
scenario: scenario,
),
]);
};
} else if (adapter is GetItAdapter || adapter.runtimeType.toString().contains('GetItScopeAdapter')) {
} as Registration<TContainer>;
}
if (adapter is GetItAdapter) {
return (getIt) {
switch (scenario) {
case UniversalScenario.asyncChain:
@@ -39,7 +42,7 @@ void Function(dynamic) getUniversalRegistration(
final prev = level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null;
return UniversalServiceImpl(value: depName, dependency: prev as UniversalService?);
return UniversalServiceImpl(value: depName, dependency: prev as UniversalService?);
},
instanceName: depName,
);
@@ -64,8 +67,8 @@ void Function(dynamic) getUniversalRegistration(
UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
@@ -75,20 +78,19 @@ void Function(dynamic) getUniversalRegistration(
() => UniversalServiceImpl(
value: depName,
dependency: level > 1
? getIt<UniversalService>(instanceName: prevDepName)
: null,
? getIt<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
break;
case UniversalBindingMode.asyncStrategy:
// getIt не поддерживает асинх. factory напрямую, но можно так:
getIt.registerSingletonAsync<UniversalService>(
() async => UniversalServiceImpl(
value: depName,
dependency: level > 1
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null,
? await getIt.getAsync<UniversalService>(instanceName: prevDepName)
: null,
),
instanceName: depName,
);
@@ -108,14 +110,11 @@ void Function(dynamic) getUniversalRegistration(
getIt<UniversalService>(instanceName: depName),
);
}
};
} as Registration<TContainer>;
}
// Riverpod
if (adapter.runtimeType.toString().contains('RiverpodAdapter')) {
// Регистрация Provider-ов по универсальному сценарию
if (adapter is DIAdapter<Map<String, rp.ProviderBase<Object?>>> && adapter.runtimeType.toString().contains('RiverpodAdapter')) {
return (providers) {
// providers это Map<String, ProviderBase<Object?>>
switch (scenario) {
case UniversalScenario.register:
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => UniversalServiceImpl(value: 'reg', dependency: null));
@@ -135,7 +134,6 @@ void Function(dynamic) getUniversalRegistration(
));
}
}
// Alias для последнего (универсальное имя)
final depName = '${chainCount}_$nestingDepth';
providers['UniversalService'] = rp.Provider<UniversalService>((ref) => ref.watch(providers[depName] as rp.ProviderBase<UniversalService>));
break;
@@ -157,15 +155,14 @@ void Function(dynamic) getUniversalRegistration(
});
}
}
// Alias для последнего (универсальное имя)
final depName = '${chainCount}_$nestingDepth';
providers['UniversalService'] = rp.FutureProvider<UniversalService>((ref) async {
return await ref.watch(providers[depName]!.future) as UniversalService;
});
break;
}
};
} as Registration<TContainer>;
}
throw UnsupportedError('Unknown DIAdapter type: ${adapter.runtimeType}');
throw UnsupportedError('Unknown DIAdapter type: ${adapter.runtimeType}');
}