Files
cherrypick/doc/cycle_detection.ru.md
Sergey Penkovsky d63d52b817 feat: implement comprehensive circular dependency detection system
- Add two-level circular dependency detection (local and global)
- Implement CycleDetector for local scope cycle detection
- Implement GlobalCycleDetector for cross-scope cycle detection
- Add CircularDependencyException with detailed dependency chain info
- Integrate cycle detection into Scope class with unique scope IDs
- Extend CherryPick helper with cycle detection management API
- Add safe scope creation methods with automatic detection
- Support both synchronous and asynchronous dependency resolution
- Include comprehensive test coverage (72+ tests)
- Add bilingual documentation (English and Russian)
- Provide usage examples and architectural best practices
- Add performance recommendations and debug tools

BREAKING CHANGE: Scope constructor now generates unique IDs for global detection

fix: remove tmp files

update examples

update examples
2025-08-01 08:31:50 +03:00

21 KiB
Raw Permalink Blame History

Обнаружение циклических зависимостей

CherryPick предоставляет надежное обнаружение циклических зависимостей для предотвращения бесконечных циклов и ошибок переполнения стека в вашей настройке внедрения зависимостей.

Что такое циклические зависимости?

Циклические зависимости возникают, когда два или более сервиса зависят друг от друга прямо или косвенно, создавая цикл в графе зависимостей.

Пример циклических зависимостей в рамках скоупа

class UserService {
  final OrderService orderService;
  UserService(this.orderService);
}

class OrderService {
  final UserService userService;
  OrderService(this.userService);
}

Пример циклических зависимостей между скоупами

// В родительском скоупе
class ParentService {
  final ChildService childService;
  ParentService(this.childService); // Получает из дочернего скоупа
}

// В дочернем скоупе
class ChildService {
  final ParentService parentService;
  ChildService(this.parentService); // Получает из родительского скоупа
}

Типы обнаружения

🔍 Локальное обнаружение

Обнаруживает циклические зависимости в рамках одного скоупа. Быстрое и эффективное.

🌐 Глобальное обнаружение

Обнаруживает циклические зависимости во всей иерархии скоупов. Более медленное, но обеспечивает полную защиту.

Использование

Локальное обнаружение

final scope = Scope(null);
scope.enableCycleDetection(); // Включить локальное обнаружение

scope.installModules([
  Module((bind) {
    bind<UserService>().to((scope) => UserService(scope.resolve<OrderService>()));
    bind<OrderService>().to((scope) => OrderService(scope.resolve<UserService>()));
  }),
]);

try {
  final userService = scope.resolve<UserService>(); // Выбросит CircularDependencyException
} catch (e) {
  print(e); // CircularDependencyException: Circular dependency detected
}

Глобальное обнаружение

// Включить глобальное обнаружение для всех скоупов
CherryPick.enableGlobalCrossScopeCycleDetection();

final rootScope = CherryPick.openGlobalSafeRootScope();
final childScope = rootScope.openSubScope();

// Настроить зависимости, которые создают межскоуповые циклы
rootScope.installModules([
  Module((bind) {
    bind<ParentService>().to((scope) => ParentService(childScope.resolve<ChildService>()));
  }),
]);

childScope.installModules([
  Module((bind) {
    bind<ChildService>().to((scope) => ChildService(rootScope.resolve<ParentService>()));
  }),
]);

try {
  final parentService = rootScope.resolve<ParentService>(); // Выбросит CircularDependencyException
} catch (e) {
  print(e); // CircularDependencyException с детальной информацией о цепочке
}

API CherryPick Helper

Глобальные настройки

// Включить/отключить локальное обнаружение глобально
CherryPick.enableGlobalCycleDetection();
CherryPick.disableGlobalCycleDetection();

// Включить/отключить глобальное межскоуповое обнаружение
CherryPick.enableGlobalCrossScopeCycleDetection();
CherryPick.disableGlobalCrossScopeCycleDetection();

// Проверить текущие настройки
bool localEnabled = CherryPick.isGlobalCycleDetectionEnabled;
bool globalEnabled = CherryPick.isGlobalCrossScopeCycleDetectionEnabled;

Настройки для конкретного скоупа

// Включить/отключить для конкретного скоупа
CherryPick.enableCycleDetectionForScope(scope);
CherryPick.disableCycleDetectionForScope(scope);

// Включить/отключить глобальное обнаружение для конкретного скоупа
CherryPick.enableGlobalCycleDetectionForScope(scope);
CherryPick.disableGlobalCycleDetectionForScope(scope);

Безопасное создание скоупов

// Создать скоупы с автоматически включенным обнаружением
final safeRootScope = CherryPick.openSafeRootScope(); // Локальное обнаружение включено
final globalSafeRootScope = CherryPick.openGlobalSafeRootScope(); // Включены локальное и глобальное
final safeSubScope = CherryPick.openSafeSubScope(parentScope); // Наследует настройки родителя

Соображения производительности

Тип обнаружения Накладные расходы Рекомендуемое использование
Локальное Минимальные (~5%) Разработка, тестирование
Глобальное Умеренные (~15%) Сложные иерархии, критические функции
Отключено Нет Продакшн (после тестирования)

Рекомендации

  • Разработка: Включите локальное и глобальное обнаружение для максимальной безопасности
  • Тестирование: Оставьте обнаружение включенным для раннего выявления проблем
  • Продакшн: Рассмотрите отключение для производительности, но только после тщательного тестирования
import 'package:flutter/foundation.dart';

void configureCycleDetection() {
  if (kDebugMode) {
    // Включить полную защиту в режиме отладки
    CherryPick.enableGlobalCycleDetection();
    CherryPick.enableGlobalCrossScopeCycleDetection();
  } else {
    // Отключить в релизном режиме для производительности
    CherryPick.disableGlobalCycleDetection();
    CherryPick.disableGlobalCrossScopeCycleDetection();
  }
}

Архитектурные паттерны

Паттерн Repository

// ✅ Правильно: Repository не зависит от сервиса
class UserRepository {
  final ApiClient apiClient;
  UserRepository(this.apiClient);
}

class UserService {
  final UserRepository repository;
  UserService(this.repository);
}

// ❌ Неправильно: Циклическая зависимость
class UserRepository {
  final UserService userService; // Не делайте так!
  UserRepository(this.userService);
}

Паттерн Mediator

// ✅ Правильно: Используйте медиатор для разрыва циклов
abstract class EventBus {
  void publish<T>(T event);
  Stream<T> listen<T>();
}

class UserService {
  final EventBus eventBus;
  UserService(this.eventBus);
  
  void createUser(User user) {
    // ... логика создания пользователя
    eventBus.publish(UserCreatedEvent(user));
  }
}

class OrderService {
  final EventBus eventBus;
  OrderService(this.eventBus) {
    eventBus.listen<UserCreatedEvent>().listen(_onUserCreated);
  }
  
  void _onUserCreated(UserCreatedEvent event) {
    // Реагировать на создание пользователя без прямой зависимости
  }
}

Лучшие практики иерархии скоупов

Правильный поток зависимостей

// ✅ Правильно: Зависимости текут вниз по иерархии
// Корневой скоуп: Основные сервисы
final rootScope = CherryPick.openGlobalSafeRootScope();
rootScope.installModules([
  Module((bind) {
    bind<DatabaseService>().singleton((scope) => DatabaseService());
    bind<ApiClient>().singleton((scope) => ApiClient());
  }),
]);

// Скоуп функции: Сервисы, специфичные для функции
final featureScope = rootScope.openSubScope();
featureScope.installModules([
  Module((bind) {
    bind<UserRepository>().to((scope) => UserRepository(scope.resolve<ApiClient>()));
    bind<UserService>().to((scope) => UserService(scope.resolve<UserRepository>()));
  }),
]);

// UI скоуп: Сервисы, специфичные для UI
final uiScope = featureScope.openSubScope();
uiScope.installModules([
  Module((bind) {
    bind<UserController>().to((scope) => UserController(scope.resolve<UserService>()));
  }),
]);

Избегайте межскоуповых зависимостей

// ❌ Неправильно: Дочерний скоуп зависит от конкретных сервисов родителя
childScope.installModules([
  Module((bind) {
    bind<ChildService>().to((scope) => 
      ChildService(rootScope.resolve<ParentService>()) // Рискованно!
    );
  }),
]);

// ✅ Правильно: Используйте интерфейсы и правильное внедрение зависимостей
abstract class IParentService {
  void doSomething();
}

class ParentService implements IParentService {
  void doSomething() { /* реализация */ }
}

// В корневом скоупе
rootScope.installModules([
  Module((bind) {
    bind<IParentService>().to((scope) => ParentService());
  }),
]);

// В дочернем скоупе - разрешение через обычную иерархию
childScope.installModules([
  Module((bind) {
    bind<ChildService>().to((scope) => 
      ChildService(scope.resolve<IParentService>()) // Безопасно!
    );
  }),
]);

Режим отладки

Отслеживание цепочки разрешения

// Включить режим отладки для отслеживания цепочек разрешения
final scope = CherryPick.openGlobalSafeRootScope();

// Доступ к текущей цепочке разрешения для отладки
print('Текущая цепочка разрешения: ${scope.currentResolutionChain}');

// Доступ к глобальной цепочке разрешения
print('Глобальная цепочка разрешения: ${GlobalCycleDetector.instance.currentGlobalResolutionChain}');

Детали исключений

try {
  final service = scope.resolve<CircularService>();
} on CircularDependencyException catch (e) {
  print('Ошибка: ${e.message}');
  print('Цепочка зависимостей: ${e.dependencyChain.join(' -> ')}');
  
  // Для глобального обнаружения доступен дополнительный контекст
  if (e.message.contains('cross-scope')) {
    print('Это межскоуповая циклическая зависимость');
  }
}

Интеграция с тестированием

Модульные тесты

import 'package:test/test.dart';
import 'package:cherrypick/cherrypick.dart';

void main() {
  group('Обнаружение циклических зависимостей', () {
    setUp(() {
      // Включить обнаружение для тестов
      CherryPick.enableGlobalCycleDetection();
      CherryPick.enableGlobalCrossScopeCycleDetection();
    });
    
    tearDown(() {
      // Очистка после тестов
      CherryPick.disableGlobalCycleDetection();
      CherryPick.disableGlobalCrossScopeCycleDetection();
    });
    
    test('должен обнаружить циклическую зависимость', () {
      final scope = CherryPick.openGlobalSafeRootScope();
      
      scope.installModules([
        Module((bind) {
          bind<ServiceA>().to((scope) => ServiceA(scope.resolve<ServiceB>()));
          bind<ServiceB>().to((scope) => ServiceB(scope.resolve<ServiceA>()));
        }),
      ]);
      
      expect(
        () => scope.resolve<ServiceA>(),
        throwsA(isA<CircularDependencyException>()),
      );
    });
  });
}

Интеграционные тесты

testWidgets('должен обрабатывать циклические зависимости в дереве виджетов', (tester) async {
  // Включить обнаружение
  CherryPick.enableGlobalCycleDetection();
  
  await tester.pumpWidget(
    CherryPickProvider(
      create: () {
        final scope = CherryPick.openGlobalSafeRootScope();
        // Настроить модули, которые могут иметь циклы
        return scope;
      },
      child: MyApp(),
    ),
  );
  
  // Проверить, что циклические зависимости правильно обрабатываются
  expect(find.text('Ошибка: Обнаружена циклическая зависимость'), findsNothing);
});

Руководство по миграции

С версии 2.1.x на 2.2.x

  1. Обновите зависимости:

    dependencies:
      cherrypick: ^2.2.0
    
  2. Включите обнаружение в существующем коде:

    // Раньше
    final scope = Scope(null);
    
    // Теперь - с локальным обнаружением
    final scope = CherryPick.openSafeRootScope();
    
    // Или с глобальным обнаружением
    final scope = CherryPick.openGlobalSafeRootScope();
    
  3. Обновите обработку ошибок:

    try {
      final service = scope.resolve<MyService>();
    } on CircularDependencyException catch (e) {
      // Обработать ошибки циклических зависимостей
      logger.error('Обнаружена циклическая зависимость: ${e.dependencyChain}');
    }
    
  4. Настройте для продакшна:

    void main() {
      // Настроить обнаружение в зависимости от режима сборки
      if (kDebugMode) {
        CherryPick.enableGlobalCycleDetection();
        CherryPick.enableGlobalCrossScopeCycleDetection();
      }
    
      runApp(MyApp());
    }
    

Справочник API

Методы Scope

class Scope {
  // Локальное обнаружение циклов
  void enableCycleDetection();
  void disableCycleDetection();
  bool get isCycleDetectionEnabled;
  List<String> get currentResolutionChain;
  
  // Глобальное обнаружение циклов
  void enableGlobalCycleDetection();
  void disableGlobalCycleDetection();
  bool get isGlobalCycleDetectionEnabled;
}

Методы CherryPick Helper

class CherryPick {
  // Глобальные настройки
  static void enableGlobalCycleDetection();
  static void disableGlobalCycleDetection();
  static bool get isGlobalCycleDetectionEnabled;
  
  static void enableGlobalCrossScopeCycleDetection();
  static void disableGlobalCrossScopeCycleDetection();
  static bool get isGlobalCrossScopeCycleDetectionEnabled;
  
  // Настройки для конкретного скоупа
  static void enableCycleDetectionForScope(Scope scope);
  static void disableCycleDetectionForScope(Scope scope);
  static void enableGlobalCycleDetectionForScope(Scope scope);
  static void disableGlobalCycleDetectionForScope(Scope scope);
  
  // Безопасное создание скоупов
  static Scope openSafeRootScope();
  static Scope openGlobalSafeRootScope();
  static Scope openSafeSubScope(Scope parent);
}

Классы исключений

class CircularDependencyException implements Exception {
  final String message;
  final List<String> dependencyChain;
  
  const CircularDependencyException(this.message, this.dependencyChain);
  
  @override
  String toString() {
    final chain = dependencyChain.join(' -> ');
    return 'CircularDependencyException: $message\nЦепочка зависимостей: $chain';
  }
}

Лучшие практики

1. Включайте обнаружение во время разработки

void main() {
  if (kDebugMode) {
    CherryPick.enableGlobalCycleDetection();
    CherryPick.enableGlobalCrossScopeCycleDetection();
  }
  
  runApp(MyApp());
}

2. Используйте безопасное создание скоупов

// Вместо
final scope = Scope(null);

// Используйте
final scope = CherryPick.openGlobalSafeRootScope();

3. Проектируйте правильную архитектуру

  • Следуйте принципу единственной ответственности
  • Используйте интерфейсы для разделения зависимостей
  • Реализуйте паттерн медиатор для сложных взаимодействий
  • Поддерживайте однонаправленный поток зависимостей в иерархии скоупов

4. Обрабатывайте ошибки корректно

T resolveSafely<T>() {
  try {
    return scope.resolve<T>();
  } on CircularDependencyException catch (e) {
    logger.error('Обнаружена циклическая зависимость', e);
    rethrow;
  }
}

5. Тестируйте тщательно

  • Пишите модульные тесты для конфигураций зависимостей
  • Используйте интеграционные тесты для проверки сложных сценариев
  • Включайте обнаружение в тестовых средах
  • Тестируйте как положительные, так и отрицательные сценарии

Устранение неполадок

Распространенные проблемы

  1. Ложные срабатывания: Если вы получаете ложные ошибки циклических зависимостей, проверьте правильность обработки async в ваших провайдерах.

  2. Проблемы производительности: Если глобальное обнаружение слишком медленное, рассмотрите использование только локального обнаружения или отключение в продакшне.

  3. Сложные иерархии: Для очень сложных иерархий скоупов рассмотрите упрощение архитектуры или использование большего количества интерфейсов.

Советы по отладке

  1. Проверьте цепочку разрешения: Используйте scope.currentResolutionChain для просмотра текущего пути разрешения зависимостей.

  2. Включите логирование: Добавьте логирование в ваши провайдеры для трассировки разрешения зависимостей.

  3. Упростите зависимости: Разбейте сложные зависимости на более мелкие, управляемые части.

  4. Используйте интерфейсы: Абстрагируйте зависимости за интерфейсами для уменьшения связанности.

Заключение

Обнаружение циклических зависимостей в CherryPick обеспечивает надежную защиту от бесконечных циклов и ошибок переполнения стека. Следуя лучшим практикам и используя подходящий уровень обнаружения для вашего случая использования, вы можете создавать надежные и поддерживаемые конфигурации внедрения зависимостей.

Для получения дополнительной информации см. основную документацию и примеры.