Olá pessoas! Este é o primeiro artigo de uma série sobre Kafka e como ele pode te ajudar na implementação de eventos na sua aplicação. Kafka, entre outras coisas, trabalha formidavelmente como message broker. E no decorrer dessa série vamos aprender a usar o Kafka nessa abordagem. Já, na minha sincera opinião, para você dominar eventos, precisa primeiro entender quando e como implementar o padrão Observer. E é exatamente isso que faremos neste artigo!
No princípio era o Pattern
Não vamos voltar ao gênesis bíblico, mas ao documento que “oficializa” a criação do pattern Observer. O já famoso e histórico catálogos de padrões “Design Patterns”. O livro é essencial para quem quer aprender a programar orientado a objetos. Neste livro temos a descrição e um caso de uso do pattern Observer. O padrão é descrito como:
Eu destaco a palavra “notificados” porque a ideia de desacoplamento não subentende “dependência” e tão pouco conhecer o que a outra entidade fará com a notificação recebida (se ela vai se atualizar ou apenas praticar outra ação). É muito intuitivo perceber o uso do Observer em aplicações com UI. Você clica no botão e algo acontece, você tecla a seta e um quadradinho se move para o lado. Você atualiza o valor de uma célula e o Excel atualiza o gráfico. Mas esse padrão não precisa estar apenas na interface. Ele pode também atuar nas regras de negócio.
Antes de prosseguir, se você vai neste exato momento abrir a sua edição de Desing Patterns, com certeza vai encontrar lá a definição de papeis e um template básico de implementação do código. Permita que eu te liberte: Seu código não precisa ser igualzinho, tá? Absorva o sentido o padrão, entenda que problema ele quer resolver e implemente da maneira mais simples possível. Há quem acredite que você precisa reproduzir igualzinho. Eu não sou essa pessoa e não apoio isso. Continuando…
Observer nas regras de negócio?
Me permita pensar no exemplo mais comum: o processo de venda. Como você codificaria a confirmação da venda?
public static void FinalizarVenda(Venda venda)
{
var pagamentoAutorizado = AutorizarPagamento(venda.FormaPagamento);
if (!pagamentoAutorizado)
{
return;
}
venda.ItensVenda.ForEach(MovimentarEstoque);
PersistirVenda(venda);
}
Vamos supor que agora, baseado no histórico do cliente, o pagamento pode não ser autorizado. Possivelmente a interface do módulo responsável pela autorização deverá ser alterada, requisitando agora os dados do cliente no contexto da loja. Então teremos de alterar a finalização da venda… Opa! Percebe que uma alteração na autorização de pagamento gerou uma pendência de alteração na finalização da venda?
Você pode pensar – e não sem razão – que isso não é um grande problema. Afinal, a autorização do pagamento e a venda tem que ter certo nível de acoplamento, já que são intimamente ligados. Hmmm… Talvez. Vamos pensar, por exemplo, que agora quem mudou foi o estoque. Para movimentar o estoque, agora é preciso que seja informado o tipo de operação que gerou a movimentação no estoque, além da data e hora.
Você pensa: ok! Só alterar a assinatura do método. Mas você tem os módulos de: entrada de produto, devolução de produtos, balanço patrimonial e transferência de estoque. Percebe em quantos locais essa alteração vai ter que ocorrer? Parece que as coisas se complicaram um pouco, não é mesmo?
E se eu alterasse o código para:
public static void FinalizarVenda(Venda venda)
{
var vendaEvent = venda.Serialize();
_broker.Notify(vendaEvent); PersistirVenda(venda);
}
A grande diferença é que agora eu tenho um objeto _broker
que é responsável por notificar todos os interessados a respeito de uma finalização de venda. Se a assinatura de um método for alterada, ou se você quiser adicionar novos passos (como o envio de um e-mail ao consumidor, por exemplo), você não precisa modificar o procedimento FinalizarVenda
. Você pode simplesmente adicionar um novo observer (para seguir a nomenclatura do pattern) ao broker.
Aplicação de exemplo
Apesar de ser inimigo do toycode, tentar escrever algo real e trivial não daria certo. Por isso, vou apresentar para vocês apenas uma aplicação console que escreve na tela as operações executadas. Vou tentar focar no código mais importante e aquele que for repetido, vou explicar só uma vez. A aplicação de exemplo estará disponível no meu GitHub.
Regras de negócio
Antes de qualquer coisa, quero escrever as regras de negócio.
Ao escrever código de negócio, eu gosto de imaginar que os módulos/namespaces/departamentos/domínios da aplicação estão brigados, só conversam por intermediários e acreditam que são auto suficientes.
No exemplo acima, não há nenhuma interdependência entre os namespaces. Sales
não conhece nada de Warehouses
e ambos não conhecem nada de Financials
e vice-versa. Assim eu garanto que mudanças internas não irão afetar códigos externos.
A regra de negócio será simples. Apenas vamos escrever no console o que está acontecendo, como no exemplo abaixo:
public class StockController
{
public void ExecuteMovement(StockMovement movement)
{
Console.WriteLine("-> StockMovement BEGIN");
Console.WriteLine($"-> StockMovement Executing {movement.MovementKind} of '{movement.Product}' product with {movement.Quantity} units");
Console.WriteLine("-> StockMovement END");
}
}
Mas embora não se conheçam, eles precisam se comunicar. Como isso é feito? É nesse momento que os eventos vão fazer sentido.
Definindo um evento
Em eventos de UI, geralmente você só precisa de seja propagada a instância do objeto que sofreu a ação e claro, o próprio handler do evento diz qual evento de tela aconteceu. O nosso caso poderia ser algo semelhante. Mas em busca de manter a independência das demais camadas e diminuir o acoplamento, eu prefiro que ao invés de propagar a fonte do evento (o source), seja propagado em seu lugar uma versão “serializada” desse mesmo evento. Um DTO com os valores importantes do evento.
É interessante que o evento contenha todas as informações necessárias para que os observers possam trabalhar apenas com os dados do seu contexto. Isso quer dizer que você não vai propagar um evento com apenas o ID da Venda. Financeiro e Estoque não devem precisar acessar as tabelas de venda para ter acesso aos detalhes dela. Programando assim, caso futuramente você tenha que extrair algum desses namespaces para um serviço, o processo ficaria bem mais fácil.
public struct SaleSold
{
public string CustomerName { get; init; }
public string CustomerDocument {get; init; }
public IEnumerable<SaleSoldItem> SaleSoldItens { get; init; }
}
public class SaleSoldItem
{
public string ProductName { get; set; }
public decimal Quantity { get; set; }
public decimal Value { get; set; }
}
Você vai perceber que a classe de evento e o modelo de vendas são bastante parecidos. No mundo real, o modelo de eventos talvez fosse uma entidade, com seus value objects, objetos complexos e tudo o mais. A classe de eventos, por sua vez, seria o mais simples possível, composta pelos tipos básicos da linguagem.
Perceba também a nomenclatura e o tempo do verbo no nome da classe. Em geral, não utilizamos verbos em nome de classes. Eu fiz isso porque quero chamar a atenção para o quão intuitivo deve ser o nome dos seus eventos. SaleSold
me passa a ideia de que a venda foi feita. E para desfazê-la, só se eu fizer uma nova operação de cancelamento da própria. Neste caso, um novo evento SaleCanceled
seria um forte candidato a ser emitido. O prefiro também é interessante, mas por motivos que veremos nos próximos artigos.
Definindo handlers
Os handlers são objetos responsáveis por manipular o evento. Essa classe, basicamente, é o observer do padrão que discutimos no início. A responsabilidade dela será de ouvir o evento e transformá-lo em algo reconhecível pelo namespace e chamar a regra de negócio.
public class WarehouseSalesSoldHandler : ISalesSoldNotify
{
public void Update(SaleSold saleSold)
{
var controller = new StockController();
var movements = From(saleSold);
foreach (var movement in movements)
{
controller.ExecuteMovement(movement);
}
}
public static IEnumerable<StockMovement> From(SaleSold saleSold) =>
saleSold.SaleSoldItens.Select(item => new StockMovement
{
MovementKind = MovementKind.Out,
Operation = "Sale",
Product = item.ProductName,
Quantity = item.Quantity,
});
}
public interface ISalesSoldNotify
{
void Update(SaleSold saleSold);
}
Observe como a interface ISalesSoldNotify está em um nível diferente de WarehouseSaleSoldHandler. Perceba também que o código dessa classe se preocupa tão somente em traduzir o evento em um modelo do domínio e chamar a regra de negócio com o objeto correto. Esse seria um bom ponto para validar o modelo.
Estou estou fazendo o código de exemplo enquanto escrevo o artigo. E me ocorreu uma pequena refatoração nas classes de negócio. Percebi que o namespace Financials tinha uma “dependência semântica” ao definir uma classe SaleAuthorizer. Autorizadores aprovam transações. E ponto. Nesse caso, alterei para:
Outras refatorações podem ocorrer, então não deixe de ver o repositório no GitHub.
Escrevendo o Subject (ou Broker)
A próxima classe que vamos escrever seria o subject de acordo com o livro Design Patterns. Contudo, eu acredito que o termo broker esteja mais atualizado com o que desenvolvemos hoje em dia. Lembre-se que o livro foi escrito nos anos 90!
Essa classe deverá se ocupar em se capaz de adicionar/remover os handlers (que do ponto de vista dela, são listeners) e também de notificar a todos da lista quando o evento ocorrer.
public interface ISaleSoldNotifier
{
void Add(ISaleSoldNotify notify);
void Remove(ISalesSoldNotify notify);
void Notify(SaleSold saleSold);
}
public class SaleSoldNotifier : ISaleSoldNotifier
{
private readonly List _listeners;
public SaleSoldNotifyer() => _listeners = new List<ISaleSoldNotify>();
public void Add(ISaleSoldNotify listener) => _listeners.Add(listener);
public void Remove(ISaleSoldNotyf listener) => _listeners.Remove(listener);
public void Notify(SaleSold saleSold) => _listeners.ForEach(listener => listener.Update(saleSold));
}
Em aplicações como essa, NullPointerException
podem ocorrer quando se esquece de remover os observers dos brokers
. Suponha que o handler
de estoque foi retirado da memória e logo em seguida um evento é dispara. Ao rolar a lista e tentar invocar a posição com o handler de memória, você terá o NullPointerException. Por isso não se esqueça de desregistrar o handler num código do mundo real.
Testando a publicação de eventos
A esta altura você já deve ter percebido que não estamos utilizando injeção de dependência. Optei por isso apenas para poder simplificar o código. Você é quem vai me dizer se gostaria de usar – ou não – injeção de dependência. E se usar, algumas decisões precisam ser tomadas.
Como por exemplo: Vou manter o meu broker como um Singleton? Se sim, o que isso vai representar em termos de memória? Percebe a árvore de objetos que você vai ter de levar para a memória se ele for um Singleton? Pensando numa API, onde você tem sessões, se o broker for Singleton, ele ficará fora do contexto da sessão – bem como as suas dependências. É realmente isso que você quer? Estas decisões precisam ser tomadas com atenção e alteradas quando for necessário.
Para fazer a mágica acontecer, basta que a gente execute os comandos:
static void Main(string[] args)
{
var broker = RetornarBrokerConfigurado();
var saleSold = VendaEfetuadanoControllerDaApp();
broker.Notify(saleSold);
Console.ReadLine();
}
Que geraram a seguinte saída no console:
-> StockMovement BEGIN -> StockMovement Executing Out of 'Feijão' product with 1 units -> StockMovement END -> StockMovement BEGIN -> StockMovement Executing Out of 'Arroz' product with 2 units -> StockMovement END -> Authorizer BEGIN -> SalesRepository BEGIN -> SalesRepository Sale persisted to customer ObserverExample.Sales.Model.Customer -> SalesRepository END -> Authorizer AUTHORIZED to Francisco Thiago de Almeida, value: 51,00 -> Authorizer END
Provocações e problemas conhecidos
Eu propositadamente introduzi alguns problemas no código. Eu vou lista aqui alguns e se você quiser discuti-los comigo, será um prazer.
- Mescla de conceitos: Algumas nomenclaturas são próprias de eventos, outras ligadas ao uso de brokers e alguns seguindo a proposta do Design Patterns. Seria interessante que você montasse um vocabulário de palavras para cada conceitos. Por exemplo: Brokers, em geral, possuem producers e consumers. Listeners? Talvez fique melhor em outro conceito. Handlers são manipuladores, mas quais outros termos fariam mais sentido serem usados em conjunto com ele? Esse “exercício” é importante porque quanto mais próximas as palavras, melhor fica a compreensão do código por outro desenvolvedor.
- Ausência de registro: Você viu que eu registrei tudo na mão e não implementei um “desregistro” dos handlers. Como você solucionaria esse problema?
- O evento é um struct: Você saberia dizer porquê eu preferi uma passagem de valor ao invés de uma passagem de referência?
- Execução paralela: olhando a saída gerada pelo programa, você vai perceber que a autorização só acontece após a confirmação de todos os passos.
- O que aconteceria se eu não tivesse aquele Console.ReadLine aguardando a execução da Task?
- Como eu poderia obrigar o sistema esperar a conclusão da Task para só então ele encerrar?
- Caso a transação não seja autorizada, qual seria a melhor forma de dar um “rollback” em tudo o que foi feito?
- Você viu mais alguma coisa que gostaria de falar?
Fique à vontade de mandar um e-mail para mim ou de escrever na sessão de comentários. Assim todo mundo participa da conversa! Que tal?
Concluindo…
Espero que possamos ter compreendido como funciona o padrão observer e como ele pode facilitar a nossa vida, diminuindo consideravelmente o acoplamento entre as classes e namespaces e tornando a aplicação muito mais flexível. Com certeza tem um custo de código um pouco maior, mas em nada se compara ao “balaio de siri” que pode ser um código todo acoplado.
No próximo artigo vamos tentar entender para que servem os message brokers e quais problemas eles buscam resolver. Até lá!