En las dos ediciones anteriores de este boletín, hablamos sobre el patrón Use Case y el patrón Repository. En esta edición profundizaremos en lo que hace que el UseCaseService sea una herramienta tan fantástica.
Cuando seguimos el patrón de casos de uso, en realidad estamos haciendo uso del patrón de diseño CommandSe abre en una nueva pestaña, ya que todos los casos de uso están diseñados para ser ejecutados utilizando el mismo método: handle().
export class GetDestinationsQry implements UseCase<void, Destination[]> { constructor( private readonly destinationRepository: DestinationRepository, private readonly destinationOrderer: DestinationOrderer, ) {} async handle(): Promise<Destination[]> { return this.destinationOrderer.order( await this.destinationRepository.findAll() ) } }
Aquí hay otro caso de uso:
export class CreateDestinationCmd implements UseCase<CreateDestination, void> { constructor(private readonly destinationRepository: DestinationRepository) {} async handle(createDestination: CreateDestination): Promise<void> { return this.destinationRepository.create(createDestination) } }
Un caso de uso debe implementar el método handle() para ser considerado un caso de uso.
¿Sabías que puedes implementar interfaces en JavaScript? El enfoque es, en mi opinión, menos robusto, pero sigue siendo factible: ¿la respuesta? Con tests. Puedes crear un test genérico que compruebe si una instancia de una clase tiene un determinado método.
export function createUseCaseTest(useCase) { describe('UseCase', () => { it('should have method handle()', async () => { expect(useCase.handle).not.be.undefined }) }) } const destinationRepository = new DestinationInMemoryRepository() const getDestinationsQry = new GetDestinationsQry( destinationRepository, new DestinationOrderer() ) createUseCaseTest(getDestinationsQry)
Sin embargo, no usar TypeScript significaría que perderíamos las comprobaciones en tiempo de compilación, el autocompletado seguro de tipos y las refactorizaciones seguras basadas en tipos.
¿Por qué?
Entonces, ¿por qué querríamos ejecutar los casos de uso de la misma manera? Bueno, porque entonces podemos añadir lógica antes, después o durante la ejecución de los casos de uso de una manera sencilla pero potente.
Permíteme presentarte el UseCaseService:
export class UseCaseService { execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { return useCase.handle(params) } }
Está pensado para ser utilizado así:
export const destinationInMemoryRepository = new DestinationInMemoryRepository() export const createDestinationCmd = new CreateDestinationCmd(destinationInMemoryRepository) export const useCaseService = new UseCaseService() const destinations = await useCaseService.execute(createDestinationCmd, newDestination)
En las próximas ediciones del boletín vamos a mejorar este contenedor de inyección de dependencias.
Bien, ¿y qué? ¿Todo este código solo para añadir otra abstracción? Bueno, ¿qué pasaría si tuviéramos que implementar el registro (logging) para cada caso de uso? Fácil.
export class UseCaseService { execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { console.log('Logging use case:', useCase.constructor.name) console.log('Logging params:', params) return useCase.handle(params) } }
Ahora cada caso de uso tiene capacidades de registro. ¿Quieres registrar también el resultado? Podemos esperar (await) al useCase.handle(params):
export class UseCaseService { async execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { console.log('Logging use case:', useCase.constructor.name) console.log('Logging params:', params) const result = await useCase.handle(params) console.log('Logging result:', result) return result } }
¿Qué pasa si necesitamos un sistema global de manejo de errores para mostrar al usuario un mensaje de error amigable?
export class UseCaseService { async execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { try { console.log('Logging use case:', useCase.constructor.name) console.log('Logging params:', params) const result = await useCase.handle(params) console.log('Logging result:', result) return result } catch (e) { alert(e.message) throw e } } }
Mejoraremos el UseCaseService para que utilicemos el patrón Chain of ResponsibilitySe abre en una nueva pestaña para mejorar cómo manejamos los intereses transversales de una manera que sea mantenible.
Con este enfoque, no necesitas seguir añadiendo try-catch por todo tu código, puedes manejarlos en un solo lugar. Si aún necesitas manejar un error particular, puedes seguir haciéndolo, pero por defecto, el sistema está preparado para manejarlos de forma genérica.
Conclusión
El UseCaseService es un lugar maravilloso para manejar características centrales transversales. A lo largo de los años lo he utilizado para resolver problemas realmente complejos de una manera sencilla. Aquí hay algunos de los casos de uso del UseCaseService que he aplicado:
- Caching
- Logging
- Capacidades offline
- Mensajes de confirmación
- Mensajes de éxito
- Reintentos
- Temporizadores
- Manejo de roles y permisos
- Estrategias de carga
- Muchos más
¿Cómo usarías tú el UseCaseService?