Saltar al contenido principal

Casos de uso

Los casos de uso son los orquestadores de la lógica de negocio. Coordinan entidades, repositorios y reglas del dominio para ejecutar una operación concreta de la aplicación. En v2, cada caso de uso implementa una de las dos interfaces base definidas en core/interfaces.

Interfaces base

Todos los casos de uso deben extender una de las dos interfaces genéricas definidas en core/interfaces:

InterfazCuándo usarla
UseCaseWithParams<Type, Params>La operación requiere datos de entrada (un objeto Params).
UseCaseWithoutParams<Type>La operación no necesita datos de entrada.

El parámetro de tipo Type debe ser siempre Result<T, AppError>.

El método que ambas interfaces obligan a implementar es execute().

Clases

Nombrado de clases

Los casos de uso deben nombrarse en PascalCase con el sufijo UseCase. El nombre base debe reflejar claramente la acción que realizan.

class GetProductsUseCase { }

class CreateOrderUseCase { }

class GetCurrentUserUseCase { }

Declaración completa

La clase declara la interfaz base con los tipos concretos de retorno y parámetro.

// Con parámetros
class GetProductsUseCase
extends UseCaseWithParams<
Result<PaginatedData<Product>, AppError>,
GetProductsParams
> { }

// Sin parámetros
class GetCurrentUserUseCase
extends UseCaseWithoutParams<Result<User?, AppError>> { }

Constructor

Inyección de dependencias

Los casos de uso deben recibir sus dependencias a través del constructor usando parámetros nombrados required. Los tipos deben ser interfaces de repositorio, nunca implementaciones concretas.

class GetProductsUseCase
extends UseCaseWithParams<
Result<PaginatedData<Product>, AppError>,
GetProductsParams
> {
GetProductsUseCase({
required IProductRepository productRepository,
}) : _productRepository = productRepository;

final IProductRepository _productRepository;
}

Múltiples dependencias

Un caso de uso puede depender de más de un repositorio cuando la operación requiere coordinar datos de distintas fuentes.

class ActivateSubscriptionUseCase
extends UseCaseWithParams<Result<void, AppError>, ActivateSubscriptionParams> {
ActivateSubscriptionUseCase({
required ISubscriptionRepository subscriptionRepository,
required IUserRepository userRepository,
}) : _subscriptionRepository = subscriptionRepository,
_userRepository = userRepository;

final ISubscriptionRepository _subscriptionRepository;
final IUserRepository _userRepository;
}

Método execute()

Implementación simple

Cuando el caso de uso delega directamente a un repositorio sin lógica adicional, execute() puede ser una expresión de una sola línea.

class GetProductsUseCase
extends UseCaseWithParams<
Result<PaginatedData<Product>, AppError>,
GetProductsParams
> {
GetProductsUseCase({required IProductRepository productRepository})
: _productRepository = productRepository;

final IProductRepository _productRepository;


Future<Result<PaginatedData<Product>, AppError>> execute(
GetProductsParams parameters,
) => _productRepository.getProducts(parameters);
}

Implementación con lógica de orquestación

Cuando la operación requiere coordinar múltiples pasos o repositorios, execute() contiene la lógica de orquestación. Dado que los repositorios ya retornan Result, no se usan bloques try/catch.

class ActivateSubscriptionUseCase
extends UseCaseWithParams<Result<void, AppError>, ActivateSubscriptionParams> {
ActivateSubscriptionUseCase({
required ISubscriptionRepository subscriptionRepository,
required IUserRepository userRepository,
}) : _subscriptionRepository = subscriptionRepository,
_userRepository = userRepository;

final ISubscriptionRepository _subscriptionRepository;
final IUserRepository _userRepository;


Future<Result<void, AppError>> execute(
ActivateSubscriptionParams parameters,
) async {
final currentUser = _userRepository.currentUser;

if (currentUser == null) {
return Failure(const AppError());
}

return _subscriptionRepository.activate(parameters);
}
}
nota

No se usan bloques try/catch en los casos de uso. Los repositorios son responsables de capturar errores y devolverlos como Failure. La capa de dominio solo orquesta el flujo.

Streams

Los casos de uso pueden exponer streams del repositorio como getters cuando la capa de presentación necesita reaccionar a cambios en tiempo real.

class GetCurrentUserUseCase
extends UseCaseWithoutParams<Result<User?, AppError>> {
GetCurrentUserUseCase({required IUserRepository userRepository})
: _userRepository = userRepository;

final IUserRepository _userRepository;

/// Emits the updated user data whenever the profile changes.
Stream<User> get currentUserStream => _userRepository.currentUserChanges;


Future<Result<User?, AppError>> execute() =>
_userRepository.getCurrentUser();
}

Estructura de archivos

Los casos de uso deben organizarse en subcarpetas por dominio dentro de use_cases/. Cada subcarpeta debe tener su propio barrel file. El directorio raíz use_cases/ debe tener un barrel file use_cases.dart que re-exporte todos los barrel files de las subcarpetas.

domain/
└── use_cases/
├── order/
│ ├── create_order.dart
│ ├── get_orders.dart
│ └── order.dart ← barrel file de subcarpeta
├── product/
│ ├── create_product.dart
│ ├── get_products.dart
│ └── product.dart ← barrel file de subcarpeta
├── user/
│ ├── get_current_user.dart
│ └── user.dart ← barrel file de subcarpeta
└── use_cases.dart ← barrel file raíz

Nombrado de archivos

Los archivos deben nombrarse en snake_case usando el nombre completo de la clase sin el sufijo UseCase.

GetProductsUseCase   →  get_products.dart
CreateOrderUseCase → create_order.dart
GetCurrentUserUseCase → get_current_user.dart

Barrel file de subcarpeta

use_cases/product/product.dart
export 'create_product.dart';
export 'get_products.dart';

Barrel file raíz

use_cases/use_cases.dart
export 'order/order.dart';
export 'product/product.dart';
export 'user/user.dart';