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 c in config.chainCounts) {
for (final d in config.nestDepths) { for (final d in config.nestDepths) {
BenchmarkResult benchResult; BenchmarkResult benchResult;
final di = config.di == 'getit' if (config.di == 'getit') {
? GetItAdapter() final di = GetItAdapter();
: config.di == 'riverpod' if (scenario == UniversalScenario.asyncChain) {
? RiverpodAdapter() final benchAsync = UniversalChainAsyncBenchmark(di,
: CherrypickDIAdapter(); chainCount: c, nestingDepth: d, mode: mode,
if (scenario == UniversalScenario.asyncChain) { );
final benchAsync = UniversalChainAsyncBenchmark(di, benchResult = await BenchmarkRunner.runAsync(
chainCount: c, nestingDepth: d, mode: mode, benchmark: benchAsync,
); warmups: config.warmups,
benchResult = await BenchmarkRunner.runAsync( repeats: config.repeats,
benchmark: benchAsync, );
warmups: config.warmups, } else {
repeats: config.repeats, 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 { } else {
final benchSync = UniversalChainBenchmark(di, final di = CherrypickDIAdapter();
chainCount: c, nestingDepth: d, mode: mode, scenario: scenario, if (scenario == UniversalScenario.asyncChain) {
); final benchAsync = UniversalChainAsyncBenchmark(di,
benchResult = await BenchmarkRunner.runSync( chainCount: c, nestingDepth: d, mode: mode,
benchmark: benchSync, );
warmups: config.warmups, benchResult = await BenchmarkRunner.runAsync(
repeats: config.repeats, 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; final timings = benchResult.timings;
timings.sort(); timings.sort();

View File

@@ -1,13 +1,18 @@
import 'package:cherrypick/cherrypick.dart'; import 'package:cherrypick/cherrypick.dart';
import 'di_adapter.dart'; import 'di_adapter.dart';
/// DIAdapter implementation for the CherryPick DI library using registration callbacks. /// Универсальный DIAdapter для CherryPick с поддержкой subScope без дублирования логики.
class CherrypickDIAdapter implements DIAdapter { class CherrypickDIAdapter extends DIAdapter<Scope> {
Scope? _scope; Scope? _scope;
final bool _isSubScope;
CherrypickDIAdapter([Scope? scope, this._isSubScope = false]) {
_scope = scope;
}
@override @override
void setupDependencies(void Function(dynamic container) registration) { void setupDependencies(void Function(Scope container) registration) {
_scope = CherryPick.openRootScope(); _scope ??= CherryPick.openRootScope();
registration(_scope!); registration(_scope!);
} }
@@ -21,45 +26,18 @@ class CherrypickDIAdapter implements DIAdapter {
@override @override
void teardown() { void teardown() {
CherryPick.closeRootScope(); if (!_isSubScope) {
_scope = null; CherryPick.closeRootScope();
_scope = null;
}
// SubScope teardown не требуется
} }
@override @override
CherrypickDIAdapter openSubScope(String name) { CherrypickDIAdapter openSubScope(String name) {
final sub = _scope!.openSubScope(name); return CherrypickDIAdapter(_scope!.openSubScope(name), true);
return _CherrypickSubScopeAdapter(sub);
} }
@override @override
Future<void> waitForAsyncReady() async {} 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-адаптера с унифицированной функцией регистрации.
/// /// Теперь для каждого адаптера задаём строгий generic тип контейнера.
/// Позволяет использовать любые DI-контейнеры: и модульные, и безмодульные. abstract class DIAdapter<TContainer> {
abstract class DIAdapter { /// Устанавливает зависимости с помощью строго типизированного контейнера.
/// Устанавливает зависимости с помощью одной функции регистрации. void setupDependencies(void Function(TContainer container) registration);
///
/// Функция принимает выбранный DI-контейнер, задаваемый реализацией.
void setupDependencies(void Function(dynamic container) registration);
/// Резолвит (возвращает) экземпляр типа [T] (по имени, если требуется). /// Резолвит (возвращает) экземпляр типа [T] (по имени, если требуется).
T resolve<T extends Object>({String? named}); T resolve<T extends Object>({String? named});
/// Асинхронно резолвит экземпляр типа [T]. /// Асинхронно резолвит экземпляр типа [T] (если нужно).
Future<T> resolveAsync<T extends Object>({String? named}); Future<T> resolveAsync<T extends Object>({String? named});
/// Уничтожает/отчищает DI-контейнер. /// Уничтожает/отчищает DI-контейнер.
void teardown(); void teardown();
/// Открывает дочерний под-scope (если применимо). /// Открывает дочерний scope и возвращает новый адаптер (если поддерживается).
DIAdapter openSubScope(String name); DIAdapter<TContainer> openSubScope(String name);
/// Ожидание готовности DI контейнера (нужно для async DI, например get_it) /// Ожидание готовности DI контейнера (если нужно для async DI).
Future<void> waitForAsyncReady(); Future<void> waitForAsyncReady() async {}
} }

View File

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

View File

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

View File

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

View File

@@ -5,23 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "73.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
source: sdk source: sdk
version: "0.3.3" version: "0.3.2"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "6.8.0"
ansi_styles: ansi_styles:
dependency: transitive dependency: transitive
description: description:
@@ -298,10 +298,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: macros name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3-main.0" version: "0.1.2-main.4"
matcher: matcher:
dependency: transitive dependency: transitive
description: description: