Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
UseCase в Clean Architecture
UseCase — это ключевой слой в Clean Architecture, который инкапсулирует бизнес-логику приложения. Это один из самых важных паттернов, которые я использую в профессиональной разработке.
Определение
UseCase (также называется Interactor) — это класс, который:
- Инкапсулирует одну бизнес-операцию
- Независим от деталей реализации (UI, БД, сеть)
- Легко тестируется в изоляции
- Переиспользуется в разных местах приложения
Архитектурный слой
┌─────────────────────────┐
│ Presentation (UI) │ ← Widgets, Screens, BLoC
├─────────────────────────┤
│ Domain (UseCase) │ ← Бизнес-логика
├─────────────────────────┤
│ Data (Repository) │ ← Источники данных
└─────────────────────────┘
UseCase находится в Domain слое — самом чистом и независимом от фреймворков.
Простой пример
// domain/repositories/user_repository.dart
abstract class UserRepository {
Future<User> getUserById(String id);
Future<void> saveUser(User user);
}
// domain/entities/user.dart
class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
}
// domain/usecases/get_user_usecase.dart
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<User> call(String userId) async {
// Бизнес-логика
if (userId.isEmpty) {
throw ArgumentError('User ID cannot be empty');
}
final user = await repository.getUserById(userId);
return user;
}
}
// Использование в BLoC
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserUseCase getUserUseCase;
UserBloc({required this.getUserUseCase}) : super(UserInitial()) {
on<FetchUserEvent>((event, emit) async {
emit(UserLoading());
try {
final user = await getUserUseCase(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
});
}
}
Паттерн: UseCase со входными параметрами
// domain/usecases/params/get_user_params.dart
class GetUserParams {
final String userId;
final bool includeDetails;
GetUserParams({
required this.userId,
this.includeDetails = false,
});
}
// domain/usecases/get_user_with_params_usecase.dart
class GetUserWithParamsUseCase {
final UserRepository repository;
GetUserWithParamsUseCase(this.repository);
Future<User> call(GetUserParams params) async {
if (params.userId.isEmpty) {
throw ArgumentError('User ID cannot be empty');
}
var user = await repository.getUserById(params.userId);
if (params.includeDetails) {
// Дополнительная обработка
user = await _enrichUserDetails(user);
}
return user;
}
Future<User> _enrichUserDetails(User user) async {
// Получи дополнительные данные
return user;
}
}
// Использование
final useCase = GetUserWithParamsUseCase(repository);
final user = await useCase(GetUserParams(
userId: '123',
includeDetails: true,
));
Сложный пример: Трансфер денег
// domain/repositories/account_repository.dart
abstract class AccountRepository {
Future<Account> getAccount(String accountId);
Future<void> updateAccount(Account account);
}
// domain/usecases/transfer_money_usecase.dart
class TransferMoneyUseCase {
final AccountRepository accountRepository;
TransferMoneyUseCase(this.accountRepository);
Future<void> call({
required String fromAccountId,
required String toAccountId,
required double amount,
}) async {
// Валидация
if (amount <= 0) {
throw ArgumentError('Amount must be greater than 0');
}
// Получи оба счета
final fromAccount = await accountRepository.getAccount(fromAccountId);
final toAccount = await accountRepository.getAccount(toAccountId);
// Проверь баланс
if (fromAccount.balance < amount) {
throw InsufficientFundsException('Not enough balance');
}
// Проверь статус счетов
if (!fromAccount.isActive || !toAccount.isActive) {
throw InactiveAccountException('Account is inactive');
}
// Выполни трансфер
fromAccount.balance -= amount;
toAccount.balance += amount;
fromAccount.lastTransaction = DateTime.now();
toAccount.lastTransaction = DateTime.now();
// Сохрани оба счета
await accountRepository.updateAccount(fromAccount);
await accountRepository.updateAccount(toAccount);
}
}
Паттерн: UseCase c результатом Either
Для обработки ошибок более функционально используй dartz package:
import 'package:dartz/dartz.dart';
// domain/failures/failures.dart
abstract class Failure {}
class NetworkFailure extends Failure {}
class ValidationFailure extends Failure {}
class ServerFailure extends Failure {}
// domain/usecases/get_user_either_usecase.dart
class GetUserEitherUseCase {
final UserRepository repository;
GetUserEitherUseCase(this.repository);
// Either<Failure, Success>
Future<Either<Failure, User>> call(String userId) async {
try {
if (userId.isEmpty) {
return Left(ValidationFailure());
}
final user = await repository.getUserById(userId);
return Right(user);
} catch (e) {
return Left(NetworkFailure());
}
}
}
// Использование в BLoC
final result = await getUserEitherUseCase(userId);
result.fold(
(failure) => emit(UserError('Failed to load user')),
(user) => emit(UserLoaded(user)),
);
Структура проекта
lib/
├── domain/
│ ├── entities/
│ │ ├── user.dart
│ │ └── post.dart
│ ├── repositories/
│ │ ├── user_repository.dart
│ │ └── post_repository.dart
│ └── usecases/
│ ├── get_user_usecase.dart
│ ├── create_user_usecase.dart
│ ├── get_posts_usecase.dart
│ └── params/
│ ├── get_user_params.dart
│ └── get_posts_params.dart
├── data/
│ ├── datasources/
│ │ ├── user_local_datasource.dart
│ │ └── user_remote_datasource.dart
│ ├── repositories/
│ │ └── user_repository_impl.dart
│ └── models/
│ └── user_model.dart
└── presentation/
├── bloc/
│ └── user_bloc.dart
├── pages/
│ └── user_page.dart
└── widgets/
└── user_widget.dart
Тестирование UseCase
test('GetUserUseCase returns user when repository succeeds', () async {
// Arrange
final mockRepository = MockUserRepository();
when(mockRepository.getUserById('123')).thenAnswer(
(_) async => User(id: '123', name: 'John', email: 'john@test.com'),
);
final useCase = GetUserUseCase(mockRepository);
// Act
final user = await useCase('123');
// Assert
expect(user.id, '123');
expect(user.name, 'John');
verify(mockRepository.getUserById('123')).called(1);
});
test('GetUserUseCase throws error when repository fails', () async {
// Arrange
final mockRepository = MockUserRepository();
when(mockRepository.getUserById('')).thenThrow(ArgumentError());
final useCase = GetUserUseCase(mockRepository);
// Act & Assert
expect(() => useCase(''), throwsArgumentError);
});
Преимущества UseCase
✅ Тестируемость — легко писать unit tests в изоляции ✅ Переиспользуемость — один UseCase, несколько UI экранов ✅ Чистота кода — бизнес-логика отделена от деталей ✅ Независимость — UseCase не знает про Flutter, BLoC, HTTP ✅ Масштабируемость — легко добавлять новые фичи ✅ Документация — UseCase — это договор между слоями
Антипаттерны
❌ UseCase с UI логикой
// Плохо
class GetUserUseCase {
Future<void> call(String userId) async {
final user = await repository.getUserById(userId);
showSnackBar('User loaded!'); // UI логика в UseCase!
}
}
❌ Слишком тонкий UseCase
// Плохо
class GetUserUseCase {
Future<User> call(String userId) async {
return await repository.getUserById(userId); // Просто проксирует!
}
}
❌ UseCase без инъекции зависимостей
// Плохо
class GetUserUseCase {
final repository = UserRepositoryImpl(); // Hardcoded!
}
Заключение
UseCase — это фундамент Clean Architecture. За 10+ лет я вижу, что проекты с хорошо спроектированными UseCases:
- Работают дольше без переписывания
- Легче масштабируются
- Проще тестируются
- Быстрее разрабатываются благодаря переиспользованию
Это инвестиция в качество, которая окупается быстро.