From 54446868e468d25efaf225413222950654a486cc Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Thu, 7 Aug 2025 13:44:39 +0300 Subject: [PATCH] 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. --- benchmark_di/lib/cli/benchmark_cli.dart | 84 ++++++++++---- .../lib/di_adapters/cherrypick_adapter.dart | 54 +++------ benchmark_di/lib/di_adapters/di_adapter.dart | 23 ++-- .../lib/di_adapters/get_it_adapter.dart | 103 ++++++++---------- .../lib/di_adapters/riverpod_adapter.dart | 64 ++++++----- .../scenarios/di_universal_registration.dart | 43 ++++---- pubspec.lock | 14 +-- 7 files changed, 193 insertions(+), 192 deletions(-) diff --git a/benchmark_di/lib/cli/benchmark_cli.dart b/benchmark_di/lib/cli/benchmark_cli.dart index 20dba80..fd26052 100644 --- a/benchmark_di/lib/cli/benchmark_cli.dart +++ b/benchmark_di/lib/cli/benchmark_cli.dart @@ -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(); diff --git a/benchmark_di/lib/di_adapters/cherrypick_adapter.dart b/benchmark_di/lib/di_adapters/cherrypick_adapter.dart index fd44e2f..d5cf865 100644 --- a/benchmark_di/lib/di_adapters/cherrypick_adapter.dart +++ b/benchmark_di/lib/di_adapters/cherrypick_adapter.dart @@ -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; - + 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 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({String? named}) => - named == null ? _subScope.resolve() : _subScope.resolve(named: named); - - @override - Future resolveAsync({String? named}) async => - named == null ? await _subScope.resolveAsync() : await _subScope.resolveAsync(named: named); - - @override - void teardown() { - // subScope teardown не требуется - } - - @override - CherrypickDIAdapter openSubScope(String name) { - return _CherrypickSubScopeAdapter(_subScope.openSubScope(name)); - } -} diff --git a/benchmark_di/lib/di_adapters/di_adapter.dart b/benchmark_di/lib/di_adapters/di_adapter.dart index 34aebd7..3543cce 100644 --- a/benchmark_di/lib/di_adapters/di_adapter.dart +++ b/benchmark_di/lib/di_adapters/di_adapter.dart @@ -1,24 +1,21 @@ -/// Абстракция для DI-адаптера с использованием функций регистрации. -/// -/// Позволяет использовать любые DI-контейнеры: и модульные, и безмодульные. -abstract class DIAdapter { - /// Устанавливает зависимости с помощью одной функции регистрации. - /// - /// Функция принимает выбранный DI-контейнер, задаваемый реализацией. - void setupDependencies(void Function(dynamic container) registration); +/// Универсальная абстракция для DI-адаптера с унифицированной функцией регистрации. +/// Теперь для каждого адаптера задаём строгий generic тип контейнера. +abstract class DIAdapter { + /// Устанавливает зависимости с помощью строго типизированного контейнера. + void setupDependencies(void Function(TContainer container) registration); /// Резолвит (возвращает) экземпляр типа [T] (по имени, если требуется). T resolve({String? named}); - /// Асинхронно резолвит экземпляр типа [T]. + /// Асинхронно резолвит экземпляр типа [T] (если нужно). Future resolveAsync({String? named}); /// Уничтожает/отчищает DI-контейнер. void teardown(); - /// Открывает дочерний под-scope (если применимо). - DIAdapter openSubScope(String name); + /// Открывает дочерний scope и возвращает новый адаптер (если поддерживается). + DIAdapter openSubScope(String name); - /// Ожидание готовности DI контейнера (нужно для async DI, например get_it) - Future waitForAsyncReady(); + /// Ожидание готовности DI контейнера (если нужно для async DI). + Future waitForAsyncReady() async {} } diff --git a/benchmark_di/lib/di_adapters/get_it_adapter.dart b/benchmark_di/lib/di_adapters/get_it_adapter.dart index e510cb5..68079e6 100644 --- a/benchmark_di/lib/di_adapters/get_it_adapter.dart +++ b/benchmark_di/lib/di_adapters/get_it_adapter.dart @@ -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 { late GetIt _getIt; - - @override - void setupDependencies(void Function(dynamic container) registration) { - _getIt = GetIt.asNewInstance(); - registration(_getIt); - } - - @override - T resolve({String? named}) => _getIt(instanceName: named); - - @override - Future resolveAsync({String? named}) async => _getIt(instanceName: named); - - @override - void teardown() => _getIt.reset(); - - @override - DIAdapter openSubScope(String name) { - // Открываем новый scope и возвращаем адаптер, который в setupDependencies будет использовать init. - return _GetItScopeAdapter(_getIt, name); - } - - @override - Future 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({String? named}) => _getIt(instanceName: named); - - @override - Future resolveAsync({String? named}) async => _getIt(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({String? named}) => + _getIt(instanceName: named); + + @override + Future resolveAsync({String? named}) async => + _getIt(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 waitForAsyncReady() async { await _getIt.allReady(); diff --git a/benchmark_di/lib/di_adapters/riverpod_adapter.dart b/benchmark_di/lib/di_adapters/riverpod_adapter.dart index 93e82e1..15dc4bd 100644 --- a/benchmark_di/lib/di_adapters/riverpod_adapter.dart +++ b/benchmark_di/lib/di_adapters/riverpod_adapter.dart @@ -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> _namedProviders; +/// Унифицированный DIAdapter для Riverpod с поддержкой scopes и строгой типизацией. +class RiverpodAdapter extends DIAdapter>> { + ProviderContainer? _container; + final Map> _namedProviders; final ProviderContainer? _parent; + final bool _isSubScope; - // Основной конструктор - RiverpodAdapter() : _parent = null { - _namedProviders = >{}; - } - - // Внутренний конструктор для дочерних скоупов - RiverpodAdapter._child(this._container, this._namedProviders, this._parent); + RiverpodAdapter({ + ProviderContainer? container, + Map>? providers, + ProviderContainer? parent, + bool isSubScope = false, + }) : _container = container, + _namedProviders = providers ?? >{}, + _parent = parent, + _isSubScope = isSubScope; @override - void setupDependencies(void Function(dynamic container) registration) { - // Для главного контейнера - _container = _parent == null + void setupDependencies(void Function(Map> container) registration) { + _container ??= _parent == null ? ProviderContainer() : ProviderContainer(parent: _parent); registration(_namedProviders); } - /// Регистрировать провайдеры нужно по имени-сервису. - /// Пример: container['SomeClass'] = Provider((ref) => SomeClass()); - @override T resolve({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 resolveAsync({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(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 diff --git a/benchmark_di/lib/scenarios/di_universal_registration.dart b/benchmark_di/lib/scenarios/di_universal_registration.dart index e25cae8..22d7666 100644 --- a/benchmark_di/lib/scenarios/di_universal_registration.dart +++ b/benchmark_di/lib/scenarios/di_universal_registration.dart @@ -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 = void Function(TContainer); + +Registration getUniversalRegistration( + DIAdapter 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; + } + 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(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(instanceName: prevDepName) - : null, + ? getIt(instanceName: prevDepName) + : null, ), instanceName: depName, ); @@ -75,20 +78,19 @@ void Function(dynamic) getUniversalRegistration( () => UniversalServiceImpl( value: depName, dependency: level > 1 - ? getIt(instanceName: prevDepName) - : null, + ? getIt(instanceName: prevDepName) + : null, ), instanceName: depName, ); break; case UniversalBindingMode.asyncStrategy: - // getIt не поддерживает асинх. factory напрямую, но можно так: getIt.registerSingletonAsync( () async => UniversalServiceImpl( value: depName, dependency: level > 1 - ? await getIt.getAsync(instanceName: prevDepName) - : null, + ? await getIt.getAsync(instanceName: prevDepName) + : null, ), instanceName: depName, ); @@ -108,14 +110,11 @@ void Function(dynamic) getUniversalRegistration( getIt(instanceName: depName), ); } - }; + } as Registration; } - // Riverpod - if (adapter.runtimeType.toString().contains('RiverpodAdapter')) { - // Регистрация Provider-ов по универсальному сценарию + if (adapter is DIAdapter>> && adapter.runtimeType.toString().contains('RiverpodAdapter')) { return (providers) { - // providers это Map> switch (scenario) { case UniversalScenario.register: providers['UniversalService'] = rp.Provider((ref) => UniversalServiceImpl(value: 'reg', dependency: null)); @@ -135,7 +134,6 @@ void Function(dynamic) getUniversalRegistration( )); } } - // Alias для последнего (универсальное имя) final depName = '${chainCount}_$nestingDepth'; providers['UniversalService'] = rp.Provider((ref) => ref.watch(providers[depName] as rp.ProviderBase)); break; @@ -157,15 +155,14 @@ void Function(dynamic) getUniversalRegistration( }); } } - // Alias для последнего (универсальное имя) final depName = '${chainCount}_$nestingDepth'; providers['UniversalService'] = rp.FutureProvider((ref) async { return await ref.watch(providers[depName]!.future) as UniversalService; }); break; } - }; + } as Registration; } - throw UnsupportedError('Unknown DIAdapter type: ${adapter.runtimeType}'); + throw UnsupportedError('Unknown DIAdapter type: ${adapter.runtimeType}'); } diff --git a/pubspec.lock b/pubspec.lock index 89c1b0a..eb70210 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" url: "https://pub.dev" source: hosted - version: "76.0.0" + version: "73.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.3" + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.8.0" ansi_styles: dependency: transitive description: @@ -298,10 +298,10 @@ packages: dependency: transitive description: name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.3-main.0" + version: "0.1.2-main.4" matcher: dependency: transitive description: