Este artigo é, em grande parte, uma **releitura** do artigo [Domain-Oriented Observability](https://martinfowler.com/articles/domain-oriented-observability.html#DomainProbe), escrito por **Pete Hodgson** em 2019, somado a visão de diversos outros autores sobre o assunto, além de minha opinião pessoal. O objetivo deste artigo é, portanto, introduzi-lo ao conceito apresentado no título, somado a um padrão de projetos capaz de ajudar você a melhor implementar **observabilidade**, de forma a não ser tão **técnica** ou **verbosa** as diversas chamadas para serviços de *log* e *frameworks* de análises em seus sistemas, trazendo uma **observabilidade relevante ao negócio** de forma **limpa** e **mais simples**. ## 1. O que é "Observabilidade"? **Observabilidade** é um conceito já bem difundido na **Engenharia de Software** que se refere à capacidade de monitorar e diagnosticar um sistema. Dentre as principais vantagens que a observabilidade introduz ao software, podemos citar três que se vê com certa frequência: 1. **Disponibilidade**, pois facilita a identificação e correção de erros, minimizando consequentemente o tempo de inatividade e o impacto nos usuários finais. 2. **Confiabilidade**, pois a observabilidade utilizada de forma preventiva é capaz de mitigar problemas de escalabilidade ou performance de uma aplicação antecipadamente. 3. **Rastreabilidade** do uso de funcionalidades de forma a se obter insumos para decisões estratégicas que podem ser tomadas no âmbito do negócio. Aqui, o curioso de se compreender é que embora a **primeira** e a **segunda** vantagem tenham o seu valor, a **terceira** é a que mais impacta o negócio. Ela é quem define o sucesso da sua aplicação. As também chamadas "***métricas de alto nível***", por serem intrinsecamente ligadas ao negócio, são as que mais entregam valor justamente pela sua definição; é através da **rastreabilidade** do uso das funcionalidades de um sistema que podemos entender se o mesmo está performando conforme os objetivos de um produto. O conceito de **Observabilidade Orientada ao Domínio** se agrega à ideia da **métrica de alto nível**, que naturalmente já trás valor ao negócio, e incrementa-a de uma forma coerente ao **código de domínio**, mesmo com a implementação da **observabilidade**. Mais adiante, entenderemos como isso funciona. ## 2. Entendendo o valor Grandes aplicações voltadas para análises de **métricas de alto nível**, como [Mixpanel](https://mixpanel.com/), acreditam em algo chamado "**Momentos de valor**" (*value moments*). Esta é uma palavra-chave que indica quais eventos de determinado produto são importantes se [instrumentar](https://www.harness.io/blog/instrumentation-guide-application). **Momentos de valor** variam, portanto, de acordo com o produto; uma aplicação voltada para soluções de assinatura eletrônica, como a [1Doc](https://1doc.com.br/), por exemplo, pode considerar um "momento de valor" a **assinatura de um contrato**. Para o **negócio** faz sentido ─, mas e para os **usuários**? Aqui, é importante destacar que o valor do seu negócio é composto do equilíbrio entre duas forças: a **intenção**, e a **expectativa**. Se a intenção é **facilitar o processo de assinatura de um contrato**, e a expectativa dos seus usuários é **exatamente esta**, você alcançou o equilíbrio. O desencontro dessas duas forças é uma perda de oportunidade, e valor consequentemente. Vale dizer também, entretanto, que esse desencontro entre a intenção e expectativa não é uma causa perdida. **Métricas de alto nível** servem para isso: recuperar e manter o valor do seu negócio de acordo com os **momentos de valor** identificados pelos seus **analistas de produto**. A partir daqui, seu papel como desenvolvedor passa a envolver a verificação da viabilidade técnica e a implementação da captura dessas métricas para que o "time de negócio" consiga lidar com os dados. ## 3. O problema Para entender o problema da implementação de uma boa **observabilidade orientada ao domínio**, imaginemos um pequeno sistema de **gerenciamento de tarefas**. Este sistema possui a capacidade de **cadastrar tarefas agendadas**, e então **executá-las de** acordo com o **agendamento**. Entretanto, devido a uma necessidade dos usuários, em certos momentos, pode ser necessário adiantar a execução de uma dessas tarefas de forma manual. Para atender a esta necessidade de "**execução adiantada**" a seguinte estrutura foi feita: - `GerenciadorTarefas`: Classe responsável pela execução de uma determinada tarefa baseando-se no código da mesma ─ é a nossa classe de "**caso de uso**"; - `RecuperadorTarefas`: Classe responsável pela abstração da recuperação das tarefas do banco de dados e retorno dos objetos de domínio ─ é a nossa classe "**repositório**". - `Tarefa`: Classe que representa uma "tarefa" no sistema ─ é a nossa "**entidade de domínio**". ```java= public class GerenciadorTarefas { private static boolean TAREFA_PROCESSADA = true; private static boolean TAREFA_NAO_PROCESSADA = false; private RecuperadorTarefas recuperadorTarefas; public GerenciadorTarefas(RecuperadorTarefas recuperadorTarefas) { this.recuperadorTarefas = recuperadorTarefas; } public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperadorTarefas.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { return TAREFA_NAO_PROCESSADA; } try { tarefa.iniciaProcesso(); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { return TAREFA_NAO_PROCESSADA; } } } ``` Este pode não ser o melhor dos exemplos, mas acredito que é válido afirmar que o código acima expresa bem a sua **lógica de domínio**. Agora, vamos aplicar no nosso método `executaTarefaPeloCodigo` a nossa **observabilidade**. Para isto, imaginemos duas bibliotecas em nosso projeto: - `Log`: É uma **biblioteca genérica** de logs, útil para atividades de **throubleshooting** por parte dos desenvolvedores. - `Analytics`: É uma **biblioteca genérica** de eventos, voltada para **metrificar interações de um usuário** a uma determinada funcionalidade. ```java= public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperadorTarefas.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { Log.warn("A tarefa %d não existe, portanto, seu processo não foi iniciado.", codigoTarefa); return TAREFA_NAO_PROCESSADA; } try { Log.info("Processo da tarefa %d foi iniciado.", codigoTarefa); Analytics.registraEvento("tarefa_iniciada", tarefa); tarefa.iniciaProcesso(); Log.info("Processo da tarefa %d foi finalizado.", codigoTarefa); Analytics.registraEvento("tarefa_finalizada", tarefa); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { Log.error(e, String.format("Processo da tarefa %d foi interrompido.", codigoTarefa)); Analytics.registraEvento("tarefa_interrompida", tarefa); return TAREFA_NAO_PROCESSADA; } } ``` Agora, além da execução da regra de negócio previamente expressa pelo código, também estamos lidando com diversas chamadas de *logs* e *análises* sobre o uso desta funcionalidade. Analisando, não do ponto de vista da instrumentação da observabilidade, mas **tecnicamente**, com certeza, a **manutenibilidade** deste código caiu. Primeiro que essa implementação, se crucial para o negócio, deveria ser garantida com **testes unitários** ─, algo que até então nem mesmo foi abordado ─, e segundo que a regra de negócio, que antes era claramente expressa, agora, está ofuscada com o uso dessas bibliotecas. Cenários como este são comuns de se ver nos mais diversos sistemas. Geralmente, não parece soar muito bem um "**código voltado à observabilidade**" e um "**código voltado ao domínio**", juntos. Então, existe solução? ## 4. A Solução Pensando na legibilidade do código escrito, instintivamente, talvez pensemos na criação de **pequenos métodos** que **abstraiam** esse conteúdo confuso de dentro do `executaTarefaPeloCodigo`, de forma a isolar o **código voltado ao domínio** do **código voltado às análises**. Importante mencionar, entretanto, que a **observabilidade** introduzida, neste caso, é um **requisito do negócio**, por tanto, mesmo sendo um "*código voltado às análises*", este continua sendo um "*código voltado ao domínio*". ![](https://i.imgur.com/fGuBM3g.png) Nem todo **código voltado ao domínio** é voltado à **observabilidade** e nem todo **código voltado à observabilidade** é voltado ao **domínio**, mas há, em alguns casos, uma intersecção entre estes, como no nosso. Por fim, a extração das `Strings` "mágicas" também é fortemente recomendado, assim, tornando mais agradável a leitura e mais fácil de entender o que cada uma representa. Talvez a introdução de alguns **ENUMs** também seja válido para abstrair o que seria os "*trackings de eventos*", como `tarefa_iniciada` e `tarefa_finalizada`, mas não vamos nos aprofundar neste assunto, pois não é o foco. ```java= public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperadorTarefas.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { metrificaQueTarefaNaoExiste(codigoTarefa); return TAREFA_NAO_PROCESSADA; } try { metrificaQueTarefaFoiIniciada(tarefa); tarefa.iniciaProcesso(); metrificaQueTarefaFoiFinalizada(tarefa); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { metrificaQueTarefaFoiInterrompida(tarefa); return TAREFA_NAO_PROCESSADA; } } private void metrificaQueTarefaNaoExiste(Integer codigoTarefa) { Log.warn(MENSAGEM_TAREFA_INEXISTENTE, codigoTarefa); } private void metrificaQueTarefaFoiIniciada(Tarefa tarefa) { Log.info(MENSAGEM_TAREFA_INICIADA, tarefa.getCodigo()); Analytics.registraEvento(TAREFA_INICIADA, tarefa); } private void metrificaQueTarefaFoiFinalizada(Tarefa tarefa) { Log.info(MENSAGEM_TAREFA_FINALIZADA, codigoTarefa); Analytics.registraEvento(TAREFA_FINALIZADA, tarefa); } private void metrificaQueTarefaFoiInterrompida(Tarefa tarefa) { Log.error(e, String.format(MENSAGEM_TAREFA_INTERROMPIDA, codigoTarefa)); Analytics.registraEvento(TAREFA_INTERROMPIDA, tarefa); } ``` Este é um bom começo, com o código de domínio voltando a ser bem escrito ─ isso, claro, se você considerar que seu "código de domínio" é apenas o método `executaTarefaPeloCodigo`. Observando nossa classe, não leva muito tempo para notarmos que fizemos uma **troca**. Se nós extrairmos de dentro do método original vários outros métodos de metrificação os quais não se encaixam com o **objetivo principal** da classe `GerenciadorTarefas`, apenas estamos "jogando o problema para debaixo do tapete". Quando algo assim acontece, geralmente é sinal de que uma nova classe está querendo emergir. Portanto, talvez, a mais simples solução envolva a **segregação** dessa classe em duas: uma para lidar com as métricas e outra para o processamento das tarefas. Nossa proposta, portanto, é da criação de uma nova classe responsável especificamente pelas **análises** e **logs** da aplicação, tal como demonstrado no desenho abaixo. Talvez o nome desta classe não seja o melhor dos exemplos, mas acredito que satisfaça seu propósito. ![](https://i.imgur.com/CnFaIo7.png) Esta também é uma boa solução pois a **segregação das responsabilidades** originais e o **encapsulamento das funções de métricas** em uma nova classe, somado à possível **injeção de dependências** introduzida, favorece o [design para testabilidade](https://medium.com/feedzaitech/writing-testable-code-b3201d4538eb) do `GerenciadorTarefas`, que é detentor das nossas **regras de domínio**. Podemos fomentar ainda mais essa ideia pensando no fato de que **Java** é uma **linguagem orientada a objetos** (POO), e a testabilidade de uma classe que utiliza de **métodos estáticos** é reduzida [caso este modifique um estado externo a si](https://enterprisecraftsmanship.com/posts/static-methods-evil/), e, geralmente, bibliotecas de logs atendem a este requisito. Desta forma, o resultado do nosso `GerenciadorTarefas` seria o seguinte: ```java= public class GerenciadorTarefas { private static boolean TAREFA_PROCESSADA = true; private static boolean TAREFA_NAO_PROCESSADA = false; private RecuperadorTarefas recuperador; private MetificadorTarefas metrificador; public GerenciadorTarefas(RecuperadorTarefas recuperador, MetificadorTarefas metrificador) { this.recuperador = recuperador; this.metrificador = metrificador; } public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperador.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { metrificador.metrificaQueTarefaNaoExiste(codigoTarefa); return TAREFA_NAO_PROCESSADA; } try { metrificador.metrificaQueTarefaFoiIniciada(tarefa); tarefa.iniciaProcesso(); metrificador.metrificaQueTarefaFoiFinalizada(tarefa); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { metrificador.metrificaQueTarefaFoiInterrompida(tarefa); return TAREFA_NAO_PROCESSADA; } } } ``` O processo de segregação da classe `GerenciadorTarefas` e o encapsulamento das métricas chama-se de **Observabilidade Orientada ao Domínio**, e a nova classe gerada é o nosso tão cobiçado **Domain Probe**. O nome deste padrão de projetos, "**Domain Probe**", remete literalmente, à "Sonda de Domínio". Este nome não poderia ser mais adequado visto que nossa classe literalmente age como uma "sonda", em uma classe que anteriormente **carecia do levantamento de métricas**. ### 4.1. Testando a Observabilidade Antes de chegar ao mérito de testar a observabilidade de fato, vamos retomar a primeira versão da nossa classe, e tentar imaginar um cenário de teste. ```java= public class GerenciadorTarefas { private static boolean TAREFA_PROCESSADA = true; private static boolean TAREFA_NAO_PROCESSADA = false; private RecuperadorTarefas recuperador; public GerenciadorTarefas(RecuperadorTarefas recuperador) { this.recuperador = recuperador; } public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperador.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { return TAREFA_NAO_PROCESSADA; } try { tarefa.iniciaProcesso(); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { return TAREFA_NAO_PROCESSADA; } } } ``` Se está acostumado a fazer este tipo de análise, rapidamente notará alguns cenários. 1. Ou **não existe** tarefa com o código informado, retornando `FALSE`; 2. Ou **existe** tarefa e seu **processamento é concluído**, retornando `TRUE`; 3. Ou **existe** tarefa e seu **processamento é interrompido**, retornando `FALSE`; Para simplificar, vamos utilizar apenas o terceiro cenário como exemplo. Abaixo, podemos observar como seria a implementação desta classe de testes. ```java= public class GerenciadorTarefasTest { private static final Integer CODIGO_TAREFA = 1; private GerenciadorTarefas gerenciadorTarefas; private RecuperadorTarefas recuperador; @BeforeEach public void setUp() { this.recuperador = Mockito.mock(RecuperadorTarefas.class); this.gerenciadorTarefas = new GerenciadorTarefas(recuperador); } @Test public void deveRetornarFalso_casoOcorraErroDeProcessamento_quandoExistirTarefaComCodigoInformado() throws TarefaInterrompidaException { doReturn(criaTarefaComExcecaoEmbutida()).when(recuperador).recuperaPeloCodigo(eq(CODIGO_TAREFA)); Boolean foiExecutado = gerenciadorTarefas.executaTarefaPeloCodigo(CODIGO_TAREFA); assertFalse(foiExecutado); } private Tarefa criaTarefaComExcecaoEmbutida() throws TarefaInterrompidaException { Tarefa tarefa = Mockito.spy(new Tarefa(CODIGO_TAREFA)); doThrow(new TarefaInterrompidaException()).when(tarefa).iniciaProcesso(); return tarefa; } } ``` Seguindo o padrão [GWT de nomenclatura](https://martinfowler.com/bliki/GivenWhenThen.html), facilmente conseguimos expressar nossa regra de negócio no teste. Vale mencionar, entretanto, que aqui estamos "abrasileirando" a escrita, tornando-o "**DCQ**" (*Deve ─ Caso ─ Quando*), e mudando um pouco a ordem. - "***Deve** retornar falso*", seria o equivalente à "***Then** returns false*"; - "***Caso** ocorra erro de processamento*", seria o equivalente à "***When** a processing error occurs*"; - "***Quando** existir tarefa com o código informado*", seria o equivalente à "***Given** an existing task with the informed code*". Re-implementando, nossa observabilidade, então, nossa classe `GerenciadorTarefas` voltaria a ser assim: ```java= public class GerenciadorTarefas { private static boolean TAREFA_PROCESSADA = true; private static boolean TAREFA_NAO_PROCESSADA = false; private RecuperadorTarefas recuperador; private MetificadorTarefas metrificador; public GerenciadorTarefas(RecuperadorTarefas recuperador, MetificadorTarefas metrificador) { this.recuperador = recuperador; this.metrificador = metrificador; } public boolean executaTarefaPeloCodigo(Integer codigoTarefa) { Tarefa tarefa = recuperador.recuperaPeloCodigo(codigoTarefa); if (tarefa == null) { metrificador.metrificaQueTarefaNaoExiste(codigoTarefa); return TAREFA_NAO_PROCESSADA; } try { metrificador.metrificaQueTarefaFoiIniciada(tarefa); tarefa.iniciaProcesso(); metrificador.metrificaQueTarefaFoiFinalizada(tarefa); return TAREFA_PROCESSADA; } catch (TarefaInterrompidaException e) { metrificador.metrificaQueTarefaFoiInterrompida(tarefa); return TAREFA_NAO_PROCESSADA; } } } ``` Algo extremamente importante de se pontuar aqui é que com o incremento da observabilidade comportamento nenhum foi alterado. Portanto, o teste anteriormente feito, continua cumprindo seu papel, mesmo estando desatualizado. No máximo, o que ocorreria neste caso é um **erro de compilação**, que já serviria de aviso aos testes que esta classe agora possui uma nova dependência. Sendo um incremento da nossa regra de negócio original, nada mais justo do que incrementar os testes garantindo as invocações corretas de nosso instrumentador. ```java= public class GerenciadorTarefasTest { private static final Integer CODIGO_TAREFA = 1; private GerenciadorTarefas gerenciadorTarefas; private RecuperadorTarefas recuperador; private MetificadorTarefas metrificador; @BeforeEach public void setUp() { this.recuperador = Mockito.mock(RecuperadorTarefas.class); this.metrificador = Mockito.mock(MetificadorTarefas.class); this.gerenciadorTarefas = new GerenciadorTarefas(recuperador, metrificador); } @Test public void deveRetornarFalso_casoOcorraErroDeProcessamento_quandoExistirTarefaComCodigoInformado() throws TarefaInterrompidaException { doReturn(criaTarefaComExcecaoEmbutida()).when(recuperador).recuperaPeloCodigo(eq(CODIGO_TAREFA)); Boolean foiExecutado = gerenciadorTarefas.executaTarefaPeloCodigo(CODIGO_TAREFA); Mockito.verify(metrificador, times(1)).metrificaQueTarefaFoiIniciada(any()); Mockito.verify(metrificador, times(1)).metrificaQueTarefaFoiInterrompida(any()); Mockito.verifyNoMoreInteractions(metrificador); assertFalse(foiExecutado); } private Tarefa criaTarefaComExcecaoEmbutida() throws TarefaInterrompidaException { Tarefa tarefa = Mockito.spy(new Tarefa(CODIGO_TAREFA)); doThrow(new TarefaInterrompidaException()).when(tarefa).iniciaProcesso(); return tarefa; } } ``` Aproveitado-se da dependência de um **instrumentador** dentro do nosso `GerenciadorTarefas`, podemos injetar uma [classe falsa](https://martinfowler.com/bliki/TestDouble.html), para verificar apenas o número de invocações de cada método. No teste acima, verificamos se os métodos `metrificaQueTarefaFoiIniciada` e `metrificaQueTarefaFoiInterrompida` foram invocados, e então garantimos que mais nenhuma outra interação é feita com nossa classe instrumentadora. Assim, no eventual surgimento de uma nova métrica, refatoração ou mudança na regra de negócio, pelo menos, temos testes que garantem aquilo que o negócio espera, ou esperava. ## 5. Opinião do Autor Quando li "**Domain-Oriented Observability**" pela primeira vez, não vi nada além do que já conhecia, portanto, não me foi revelador. Após algumas conversas com colegas próximos, e mais algumas tentativas de compreender a totalidade do artigo, fica claro que o subestimei. **Domain Probe** não é sobre encapsulamento, segregação ou injeção de dependência ─ embora estes sejam todos elementos que o compõem ─, mas sim sobre a **importância das métricas**, e sua **relevância para o negócio**. O padrão de projeto **Domain Probe**, embora compartilhe algumas semelhanças com um [Facade](https://refactoring.guru/design-patterns/facade), ele se preocupa com a essência de todo sistema: o **domínio**. Por isto, ele tem seu valor. Este é um padrão de projetos essencial de se conhecer e aplicar onde quer que haja **instrumentos de métricas** em um **domínio** que não tenha sido feito ou pensado para ser fácil de ler, interpretar, ou dar manutenção. Afinal, [desenvolvedores passam mais tempo lendo código do que escrevendo](https://echobind.com/post/why-should-i-write-clean-code). Outra coisa que pode passar despercebido, é que este é um padrão de projetos com extrema flexibilidade no quesito de **granularidade**. Isto é: pode-se criar desde um **Domain Probe** para cada classe de domínio, sendo esta abordagem mais "específica", a até mesmo um **Domain Probe** "genérico". Não existe uma abordagem "errada", apenas diferentes abordagens. Outro tipo de implementação de uma **Observabilidade Orientada ao Domínio**, é através de eventos. Neste cenário, o padrão de projetos da vez é o [Observer](https://refactoring.guru/design-patterns/observer), e sua abordagem é igualmente interessante. Não optei por comentar a respeito desta implementação também, pois isto resultaria em um artigo enorme, e sei bem que este já está bem grande. Por fim, agradeço **você**, caro leitor, pelo seu **tempo** e **interesse**! Recomendo, de forma complementar, uma leitura nos links presentes no decorrer do artigo, e convido-o para deixar um comentário abaixo. Sinta-se livre para deixar **sugestões**, tirar **dúvidas**, ou abrir um **debate**.