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

573 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Обнаружение циклических зависимостей
CherryPick предоставляет надежное обнаружение циклических зависимостей для предотвращения бесконечных циклов и ошибок переполнения стека в вашей настройке внедрения зависимостей.
## Что такое циклические зависимости?
Циклические зависимости возникают, когда два или более сервиса зависят друг от друга прямо или косвенно, создавая цикл в графе зависимостей.
### Пример циклических зависимостей в рамках скоупа
```dart
class UserService {
final OrderService orderService;
UserService(this.orderService);
}
class OrderService {
final UserService userService;
OrderService(this.userService);
}
```
### Пример циклических зависимостей между скоупами
```dart
// В родительском скоупе
class ParentService {
final ChildService childService;
ParentService(this.childService); // Получает из дочернего скоупа
}
// В дочернем скоупе
class ChildService {
final ParentService parentService;
ChildService(this.parentService); // Получает из родительского скоупа
}
```
## Типы обнаружения
### 🔍 Локальное обнаружение
Обнаруживает циклические зависимости в рамках одного скоупа. Быстрое и эффективное.
### 🌐 Глобальное обнаружение
Обнаруживает циклические зависимости во всей иерархии скоупов. Более медленное, но обеспечивает полную защиту.
## Использование
### Локальное обнаружение
```dart
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
}
```
### Глобальное обнаружение
```dart
// Включить глобальное обнаружение для всех скоупов
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
### Глобальные настройки
```dart
// Включить/отключить локальное обнаружение глобально
CherryPick.enableGlobalCycleDetection();
CherryPick.disableGlobalCycleDetection();
// Включить/отключить глобальное межскоуповое обнаружение
CherryPick.enableGlobalCrossScopeCycleDetection();
CherryPick.disableGlobalCrossScopeCycleDetection();
// Проверить текущие настройки
bool localEnabled = CherryPick.isGlobalCycleDetectionEnabled;
bool globalEnabled = CherryPick.isGlobalCrossScopeCycleDetectionEnabled;
```
### Настройки для конкретного скоупа
```dart
// Включить/отключить для конкретного скоупа
CherryPick.enableCycleDetectionForScope(scope);
CherryPick.disableCycleDetectionForScope(scope);
// Включить/отключить глобальное обнаружение для конкретного скоупа
CherryPick.enableGlobalCycleDetectionForScope(scope);
CherryPick.disableGlobalCycleDetectionForScope(scope);
```
### Безопасное создание скоупов
```dart
// Создать скоупы с автоматически включенным обнаружением
final safeRootScope = CherryPick.openSafeRootScope(); // Локальное обнаружение включено
final globalSafeRootScope = CherryPick.openGlobalSafeRootScope(); // Включены локальное и глобальное
final safeSubScope = CherryPick.openSafeSubScope(parentScope); // Наследует настройки родителя
```
## Соображения производительности
| Тип обнаружения | Накладные расходы | Рекомендуемое использование |
|-----------------|-------------------|----------------------------|
| **Локальное** | Минимальные (~5%) | Разработка, тестирование |
| **Глобальное** | Умеренные (~15%) | Сложные иерархии, критические функции |
| **Отключено** | Нет | Продакшн (после тестирования) |
### Рекомендации
- **Разработка**: Включите локальное и глобальное обнаружение для максимальной безопасности
- **Тестирование**: Оставьте обнаружение включенным для раннего выявления проблем
- **Продакшн**: Рассмотрите отключение для производительности, но только после тщательного тестирования
```dart
import 'package:flutter/foundation.dart';
void configureCycleDetection() {
if (kDebugMode) {
// Включить полную защиту в режиме отладки
CherryPick.enableGlobalCycleDetection();
CherryPick.enableGlobalCrossScopeCycleDetection();
} else {
// Отключить в релизном режиме для производительности
CherryPick.disableGlobalCycleDetection();
CherryPick.disableGlobalCrossScopeCycleDetection();
}
}
```
## Архитектурные паттерны
### Паттерн Repository
```dart
// ✅ Правильно: 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
```dart
// ✅ Правильно: Используйте медиатор для разрыва циклов
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) {
// Реагировать на создание пользователя без прямой зависимости
}
}
```
## Лучшие практики иерархии скоупов
### Правильный поток зависимостей
```dart
// ✅ Правильно: Зависимости текут вниз по иерархии
// Корневой скоуп: Основные сервисы
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>()));
}),
]);
```
### Избегайте межскоуповых зависимостей
```dart
// ❌ Неправильно: Дочерний скоуп зависит от конкретных сервисов родителя
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>()) // Безопасно!
);
}),
]);
```
## Режим отладки
### Отслеживание цепочки разрешения
```dart
// Включить режим отладки для отслеживания цепочек разрешения
final scope = CherryPick.openGlobalSafeRootScope();
// Доступ к текущей цепочке разрешения для отладки
print('Текущая цепочка разрешения: ${scope.currentResolutionChain}');
// Доступ к глобальной цепочке разрешения
print('Глобальная цепочка разрешения: ${GlobalCycleDetector.instance.currentGlobalResolutionChain}');
```
### Детали исключений
```dart
try {
final service = scope.resolve<CircularService>();
} on CircularDependencyException catch (e) {
print('Ошибка: ${e.message}');
print('Цепочка зависимостей: ${e.dependencyChain.join(' -> ')}');
// Для глобального обнаружения доступен дополнительный контекст
if (e.message.contains('cross-scope')) {
print('Это межскоуповая циклическая зависимость');
}
}
```
## Интеграция с тестированием
### Модульные тесты
```dart
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>()),
);
});
});
}
```
### Интеграционные тесты
```dart
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. **Обновите зависимости**:
```yaml
dependencies:
cherrypick: ^2.2.0
```
2. **Включите обнаружение в существующем коде**:
```dart
// Раньше
final scope = Scope(null);
// Теперь - с локальным обнаружением
final scope = CherryPick.openSafeRootScope();
// Или с глобальным обнаружением
final scope = CherryPick.openGlobalSafeRootScope();
```
3. **Обновите обработку ошибок**:
```dart
try {
final service = scope.resolve<MyService>();
} on CircularDependencyException catch (e) {
// Обработать ошибки циклических зависимостей
logger.error('Обнаружена циклическая зависимость: ${e.dependencyChain}');
}
```
4. **Настройте для продакшна**:
```dart
void main() {
// Настроить обнаружение в зависимости от режима сборки
if (kDebugMode) {
CherryPick.enableGlobalCycleDetection();
CherryPick.enableGlobalCrossScopeCycleDetection();
}
runApp(MyApp());
}
```
## Справочник API
### Методы Scope
```dart
class Scope {
// Локальное обнаружение циклов
void enableCycleDetection();
void disableCycleDetection();
bool get isCycleDetectionEnabled;
List<String> get currentResolutionChain;
// Глобальное обнаружение циклов
void enableGlobalCycleDetection();
void disableGlobalCycleDetection();
bool get isGlobalCycleDetectionEnabled;
}
```
### Методы CherryPick Helper
```dart
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);
}
```
### Классы исключений
```dart
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. Включайте обнаружение во время разработки
```dart
void main() {
if (kDebugMode) {
CherryPick.enableGlobalCycleDetection();
CherryPick.enableGlobalCrossScopeCycleDetection();
}
runApp(MyApp());
}
```
### 2. Используйте безопасное создание скоупов
```dart
// Вместо
final scope = Scope(null);
// Используйте
final scope = CherryPick.openGlobalSafeRootScope();
```
### 3. Проектируйте правильную архитектуру
- Следуйте принципу единственной ответственности
- Используйте интерфейсы для разделения зависимостей
- Реализуйте паттерн медиатор для сложных взаимодействий
- Поддерживайте однонаправленный поток зависимостей в иерархии скоупов
### 4. Обрабатывайте ошибки корректно
```dart
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 обеспечивает надежную защиту от бесконечных циклов и ошибок переполнения стека. Следуя лучшим практикам и используя подходящий уровень обнаружения для вашего случая использования, вы можете создавать надежные и поддерживаемые конфигурации внедрения зависимостей.
Для получения дополнительной информации см. [основную документацию](../README.md) и [примеры](../example/).