Desacoplando chamadas com Mediator
Guilherme Camargo
Posted on February 11, 2020
Quando precisamos fazer chamadas em pontos da aplicação e não temos uma boa definição para isso, podemos acabar criando dependências que não façam sentido e deixar nosso código acoplado. Neste artigo iremos ver como trabalhar com o pattern Mediator e Command a fim de minimizar este problema e ter um design mais limpo.
Se você estiver pensando algo do tipo: "Ei, mas eu já consigo desacoplar meu código com interfaces". Sim, é comum (e recomendável) utilizar IoC através de interfaces a fim de ter mais flexibilidade.
Mas te pergunto: Suas interfaces mudam? Com que frequência?
Intefaces são muito importantes em linguagens fortemente tipadas, mas não devemos usá-las como a única solução para todos nossos problemas. Elas não foram feitas para serem alteradas com frequência, seu objetivo é definir um contrato para que qualquer classe que queira implementá-la tenha a mesma assinatura, nos permitindo assim substituir seu comportamento de maneira fácil onde ela é requisitada.
Qualquer alteração em uma interface levará a um refactor considerável em N partes do código.
Porém em certos pontos da nossa aplicação devemos ter mais flexibilidade. Tome como exemplo a camada mais externa de um sistema, seu objetivo é expor os dados para o client. Inevitavelmente vamos ter que mudar parâmetros, adicionar novos campos e fazer alterações baseadas em novas necessidades. Interfaces acabam sendo um inconveniente neste cenário.
O pattern Mediator pode ser uma boa alternativa neste caso. Ele foi catalogado no conhecido livro "Design Patterns: Elements of Reusable Object-Oriented Software, GOF". Ele é, literalmente, um mediador entre duas classes que não se conhecem, nos permitindo realizar chamadas entre dois pontos sem criar acoplamento. Essas chamadas podem ser feitas através de um Command - outro pattern que teve a mesma origem. O Command trata de encapsular uma chamada através de uma classe que será enviada para execução.
Um pacote bem conhecido que implementa este pattern é o MediatR. Iremos utilizá-lo neste exemplo.
SHOW ME THE CODE!
Nosso projeto irá conter quatro csprojs, um web chamado API e três classlib chamados Application, Domain e Persistence. Todo o código exemplificado aqui está no meu GitHub.
guisfits / decoupling-calls-with-mediatr
How to make calls in a simple way and make your code more organized.
Tendo o dotnet cli instalado em sua máquina, vamos adicionar o MediatR ao nosso projeto e logo em seguida o pacote para configurá-lo.
dotnet add API package MediatR
dotnet add API package MediatR.Extensions.Microsoft.DependencyInjection
Vamos habilitar o MediatR através da classe startup na nossa API. Iremos primeiro carregar o assemply onde os handlers estaram implementados e passa-lo para o método AddMediatR.
Repare que boa parte do código original foi omitido a fim de simplificar este exemplo. Também irei omitir processos não relacionados a este artigo, para mais detalhes consulte o repositório.
using MediatR;
namespace API
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var applicationAssembly = AppDomain.CurrentDomain.Load("Application");
services.AddMediatR(applicationAssembly);
}
}
}
Na camada Application também temos que adicionar o pacote MediatR a fim de utilizarmos as interfaces IRequest e IRequestHandler.
Um esclarecimento. O MediatR chama de Request o ato de disparar uma classe para processamento. Não o confunda com HttpRequest.
dotnet add Application package MediatR
A interface IRequest tem como objetivo marcar nosso Command para ser processado por um Handler. Também podemos utilizar o overload com generics de IRequest<>, onde passamos T como tipo de retorno que o Handler nos dará deste Command.
Dentro de Application iremos criar um comando para adicionar um novo usuário.
using MediatR;
namespace Application.Commands
{
public class CreateUserCommand : IRequest
{
public string Name { get; set; }
}
}
Na camada Domain iremos adicionar nossa entidade User e seu repository, IUsers.
public class User
{
public User(string name)
{
Name = name;
}
public int Id { get; private set; }
public string Name { get; private set; }
}
public interface IUsers
{
Task Add(User newUser);
}
Feito isso iremos para a parte mais importante, o Handler!
Vamos voltar para Application e criar uma classe chamada UserCommandHandler
class UserCommandHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUsers _users;
public UserCommandHandler(IUsers users)
{
_users = users;
}
public async Task<Unit> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
Validate(request);
var user = new User(request.Name);
await _users.Add(user);
return Unit.Value;
}
private void Validate(CreateUserCommand command)
{
if (string.IsNullOrEmpty(command.Name))
throw new ArgumentException(nameof(command.Name));
}
}
Vamos testar
Em API vamos criar UserController, obter a interface IMediator e enviar o command para execução.
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(CreateUserCommand body)
{
await _mediator.Send(body);
return Created("", body);
}
}
Agora vamos fazer uma requisição para a rota criada
Como podemos ver no print, nosso request chegou até o handler.
Agora confirmamos o retorno com sucesso.
Pontos de Atenção
- Repare que nosso Handler é uma classe privada, não dando a chance de classes externas o chamarem;
- Como o Handler é uma classe comum do C#, podemos receber as dependências através de IoC;
- Seguindo o Single Responsibility Principle, seria uma boa prática ter somente um Command por Handler. Assim segmentamos melhor nossas dependências e podemos evoluir a aplicação de maneira isolada;
- Não precisamos dizer explicitamente qual Handler implementa qual Command. O MediatR é esperto o suficiente pra fazer isso automaticamente pra gente;
Recomendações
- CQRS: MediatR acaba nos ajudando na separação entre Commands e Queries;
- Event Driven: MediatR tem outra interface chamada INotification para lançar eventos, pode ser uma boa alternativa em casos simples;
- IPipelineBehavior: Farei outro artigo em breve sobre essa funcionalidade;
Conclusão
Vimos neste artigo um simples exemplo de como utilizar o pattern Mediator e Command através do MediatR a fim de um design mais elegante entre as chamadas da nossa aplicação.
Não se esqueça de procurar a documentação oficial para mais detalhes e fique atento à mudanças em futuras versões.
Caso tenha gostado não deixe de curtir, comentar e divulgar este post se possível. Também fique a vontade para me seguir por aqui, no GitHub e me adicionar no Linkedin. Este é o meu primeiro artigo técnico, pretendo escrever muitos outros daqui pra frente.
Espero que esta leitura tenha sido de alguma ajuda pra você.
Um grande e abraço e até a próxima.
Posted on February 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.