Subscribe to my newsletter
Get notified about new articles, talks, and updates directly to your inbox.
The command pattern helps us encapsulate requests in order to perform certain operations, like logging, queuing and filtering.
We start with the interface:
export interface Command<T> { execute(): Promise<T> }
And then we can look at a specific command, for example the one used to retrieve this article:
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, ) } }
This command is responsible for obtaining a certain article using a repository, where and how do we get this data we neither know nor care, that's responsibility of another class.
This command represents a Use Case of my application. Right now it only needs to get the article from the repository but in the feature it could handle if a user has read the article, or if the user is a PRO user and then can read all articles instead of a subset of articles or anything we'd like.
Who builds the command? Whoever uses it:
const article = await GetArticle.create({ id: 'use-cases-and-commands', locale: Locale.EN, }).execute()
I'm using inversion of control to provide the dependencies needed for the GetArticle use case to work. In this case I'm going from an abstraction (ArticlesRepository) to a concreation (ArticlesFileRepository). If tomorrow I decide to serve the articles via API I would only need to change the factory.
What is also interesting about commands is that they are easily augmented. For example we can log when a command is executed without touching any commands using the Decorator Pattern:
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() } }
Then, using a UserCaseDecorator
I specify which decorators I want for all my use cases:
import { Command } from '../../infrastructure/Command' import { LoggerCommandDecorator } from '../../infrastructure/LoggerCommandDecorator' import { Logger } from '../../infrastructure/Logger' export class UseCaseDecorator { private static readonly logger = Logger.create({ // eslint-disable-next-line stdout: { error: console.error, info: console.log, warn: console.warn }, }) static decorate<T>(command: Command<T>) { return new LoggerCommandDecorator<T>(command, UseCaseDecorator.logger) } }
And then in each use case we use the UseCaseDecorator
like so:
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, ), ) } }
And we could create as many decorators as we want and use composition to give more behaviour to our commands.