LogoCésar Alberca
- 7 minutos

Casos de Uso y Patrón Command

Diseña Mejor Arquitectura Frontend
Patrones de Arquitectura Frontend complejos explicados de forma sencilla. Envíos recurrentes y directos a tu correo. Conviértete en un mejor Frontend Architect.
Respeto tu privacidad. Cancela en cualquier momento.

La newsletter está escrita en inglés.

El patrón commandSe abre en una nueva pestaña nos ayuda a encapsular peticiones para realizar ciertas operaciones, como registro (logging), encolado y filtrado.

Empezamos con la interfaz:

export interface Command<T> { execute(): Promise<T> }

Y luego podemos ver un comando específico, por ejemplo el utilizado para recuperar este artículo:

import { Command } from '../../infrastructure/Command' import { Article, ArticlesRepository } from '../../domain/articles' import { Id } from '../../domain' import { Locale, Translator } from '../../domain/language' import { ArticlesFileRepository } from '../../infrastructure/articles/ArticlesFileRepository' import { FileLoader } from '../../infrastructure/FileLoader' import { TranslationService } from '../../domain/TranslationService' export class GetArticle implements Command<Article> { constructor( private readonly articlesRepository: ArticlesRepository, private readonly id: Id, private readonly locale: Locale, ) {} async execute(): Promise<Article> { return this.articlesRepository.findOneByLocale(this.id, this.locale) } static create(context: { id: Id; locale: Locale }) { return new GetArticle( new ArticlesFileRepository(FileLoader.create(), TranslationService.create(Translator.create())), context.id, context.locale, ) } }

Este comando es responsable de obtener un determinado artículo utilizando un repositorioSe abre en una nueva pestaña; dónde y cómo obtenemos estos datos ni lo sabemos ni nos importa, eso es responsabilidad de otra clase.

Este comando representa un Caso de UsoSe abre en una nueva pestaña de mi aplicación. En este momento solo necesita obtener el artículo del repositorio, pero en el futuro podría manejar si un usuario ha leído el artículo, o si el usuario es un usuario PRO y entonces puede leer todos los artículos en lugar de un subconjunto, o cualquier cosa que queramos.

¿Quién construye el comando? Quienquiera que lo use:

const article = await GetArticle.create({ id: 'use-cases-and-commands', locale: Locale.EN, }).execute()

Estoy usando inversión de controlSe abre en una nueva pestaña para proporcionar las dependencias necesarias para que el caso de uso GetArticle funcione. En este caso voy de una abstracción (ArticlesRepository) a una concreción (ArticlesFileRepository). Si mañana decido servir los artículos a través de una API, solo necesitaría cambiar la factoría.

Lo que también es interesante de los comandos es que son fácilmente aumentables. Por ejemplo, podemos registrar cuándo se ejecuta un comando sin tocar ningún comando utilizando el Patrón DecoratorSe abre en una nueva pestaña:

import { Command } from './Command' import { Logger } from './Logger' export class LoggerCommandDecorator<T> implements Command<T> { constructor( private readonly decoratedCommand: Command<T>, private readonly logger: Logger, ) {} execute(): Promise<T> { this.logger.log( (this.decoratedCommand as Object).constructor.name + ' - ' + Object.getOwnPropertyNames(this.decoratedCommand), ) return this.decoratedCommand.execute() } }

Luego, utilizando un UserCaseDecorator, especifico qué decoradores quiero para todos mis casos de uso:

import { Command } from '../../infrastructure/Command' import { LoggerCommandDecorator } from '../../infrastructure/LoggerCommandDecorator' import { Logger } from '../../infrastructure/Logger' export class UseCaseDecorator { private static readonly logger = Logger.create({ stdout: { error: console.error, info: console.log, warn: console.warn }, }) static decorate<T>(command: Command<T>) { return new LoggerCommandDecorator<T>(command, UseCaseDecorator.logger) } }

Y luego, en cada caso de uso, utilizamos el UseCaseDecorator de esta manera:

import { Command } from '../../infrastructure/Command' import { Article, ArticlesRepository } from '../../domain/articles' import { Id } from '../../domain' import { Locale, Translator } from '../../domain/language' import { UseCaseDecorator } from './UseCaseDecorator' import { ArticlesFileRepository } from '../../infrastructure/articles/ArticlesFileRepository' import { FileLoader } from '../../infrastructure/FileLoader' import { TranslationService } from '../../domain/TranslationService' export class GetArticle implements Command<Article> { constructor( private readonly articlesRepository: ArticlesRepository, private readonly id: Id, private readonly locale: Locale, ) {} async execute(): Promise<Article> { return this.articlesRepository.findOneByLocale(this.id, this.locale) } static create(context: { id: Id; locale: Locale }) { return UseCaseDecorator.decorate( new GetArticle( new ArticlesFileRepository(FileLoader.create(), TranslationService.create(Translator.create())), context.id, context.locale, ), ) } }

Y podríamos crear tantos decoradores como quisiéramos y usar la composición para dar más comportamiento a nuestros comandos.

Gracias por leer este post
¿Quieres apoyar ideas que merecen ser compartidas? Apoyar este blog me compra tiempo para escribir.
Más tiempo significa más valor para ti.