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';