Go + Repository + Command-Query Separation

Artigo 4

John Fercher
6 min readJun 26, 2023

Postman collection, código do artigo, diff com o artigo 3.

Sumário: Desenvolvimento de APIs em Go

Próximo Artigo: Go + MySQL — Docker-compose em ação

Artigo Anterior: Go + Docker — Dockerizando uma API

Introdução

Quantos tipos de bancos de dados existem? Vamos elencar alguns: bancos relacionais, bancos de documentos, bancos chave-valor, bancos de grafos, datalakes, … N tipos.

Dentro de um tipo de banco de dados, quantos bancos existem que “fazem a mesma coisa”? Vamos listar alguns bancos relacionais: MySQL, PostgreSQL, SQLServer, … M bancos.

O universo de possibilidades de integrações com bancos de dados pode ser considerado de grau M x N. Ou seja, são muitas possibilidades. Isso é bom e ruim. O lado bom é que para cada problema a ser resolvido, temos muitas opções para escolher. O lado ruim é que cada banco tem suas particularidades e, se não tivermos cuidado na hora de integrar com eles, podemos deixar o código uma verdadeira bagunça. Para ajudar a manter nosso código organizado nesse cenário de integrações com bancos de dados, temos dois padrões que podemos utilizar: o padrão Repository e o Command-Query Separation.

Repository

Em comparação ao artigo anterior, criamos mais um pacote dentro de productdomain, no caso o pacote productrepositories que carregará a implementação do repository. Ao analisar o código presente na nova struct, percebe-se que praticamente todo o código de manipulação do nosso “banco de dados” foi movido para dentro do repository. Isso ocorreu porque estamos lidando com “detalhes técnicos” do código, e não com regras de negócio em si. Na verdade, até o momento, nossa API não possui nenhuma regra de negócio implementada.

Quando menciono “detalhes técnicos”, estou me referindo a tudo que não possui relação direta com as regras de negócio. No caso específico, product.Memory, que é nosso banco de dados, expõe o fato de estarmos trabalhando com um banco em memória. Esse tipo de detalhe não deveria estar na camada de domínio (productdomain/productservices), onde nos concentramos exclusivamente na lógica da regra de negócio. Portanto, movemos a responsabilidade de lidar com product.Memory para dentro do nosso repository.

Pode parecer que apenas movemos as coisas de lugar, mas a adoção do padrão repository nos proporciona o benefício de encapsular os detalhes técnicos de acesso a dados em uma camada separada, evitando que esses detalhes influenciem nas outras camadas do projeto. Em um software “comercial”, é sempre uma boa prática buscar isolar a camada de domínio dos detalhes de implementação, independentemente de quais sejam eles. No futuro, abordaremos com mais detalhes esses conceitos ao falar sobre Domain-Driven Design e Arquitetura em Camadas.

O padrão repository tem como objetivo tornar as chamadas ao banco de dados semelhantes ao acesso a dados em memória, evitando vazamento de informações sobre a tecnologia subjacente. Além de tornar o código na camada de negócio mais coeso, esse padrão nos oferece a vantagem de possibilitar a troca do código do repository por quaisquer tecnologias (como MySQL, ElasticSearch, Redis, etc.) sem a necessidade de alterar as outras camadas. Como que isso é possível? basta olhar a assinatura dos métodos.

Os objetos utilizados na assinatura dos métodos não deixam vazar nenhum detalhe de implementação, quem utiliza isso de fora, não faz ideia que estamos utilizando um banco em memória, pois o código depende somente de productentities, context.Context e strings. Por outro lado, se estivéssemos utilizando um banco MySQL e retornássemos ao invés da entidade de domínio, um objeto de acesso a dados (DAO), estaríamos acoplando os códigos dependentes ao banco MySQL. A seguir um exemplo de DAO utilizando o GORM.

Exemplo de um DAO do GORM carregado com anotations utilizadas na serialização/deserialização dos bytes que vem do banco de dados.

Retornar um DAO do repository teria o efeito de expor detalhes técnicos na camada de negócio, comprometendo o encapsulamento, aumentando o acoplamento, diminuindo a coesão e impossibilitando a troca entre outros bancos de dados.

Por exemplo, se precisássemos migrar de um banco de dados MySQL para um banco de dados de chave-valor (ou vice-versa), poderíamos ter que adicionar ou remover anotações no objeto. Além disso, se outras camadas começassem a depender desses detalhes de implementação específicos do MySQL, o trabalho de migração poderia se tornar enormemente complexo. Já tive experiência em uma migração em que esse padrão foi extremamente útil.

Devo delegar a criação do ID da entidade para o Banco de Dados?

Muitos bancos de dados possuem uma funcionalidade em que você delega a eles a responsabilidade de gerar um ID para o objeto a ser inserido. Embora essa seja uma funcionalidade interessante, se você pretende adotar o padrão de repository, é recomendado que você pare de utilizá-la.

Como nem todos os bancos possuem essa feature, se você precisar migrar para outro banco que não possui essa criação de ID, adivinha o que vai ocorrer? terá que realizar mudanças a fim de suprir a feature que não estará presente. Que é o que estamos tentando evitar.

Command-Query Separation (CQS)

A primeira observação importante a ser feita é que CQS (Command Query Separation) não deve ser confundido com CQRS (Command Query Responsibility Segregation). Embora haja uma pequena relação entre esses conceitos, no sentido de que se um código segue os princípios do CQS, será um pouco mais fácil aplicar o CQRS com base nele. Dito isso, vamos prosseguir.

O padrão CQS tem como objetivo principal tornar claro qual é a ação realizada pelos métodos e ajudar a evitar surpresas durante o desenvolvimento. Esse padrão define dois conceitos principais: Query e Command.

  1. Query: uma ação que lê dados, retorna informações e não altera estado.
  2. Command: uma ação que altera estado e não retorna nada.

Vamos voltar as assinaturas dos métodos do repository.

Devido a Go não possuir o conceito de exceptions, nossos Commands (Create, Update e Delete) não são puros, ou seja, eles retornam dados. Como no mundo real, integramos com bancos de dados e realizamos chamadas I/O a esses bancos, sempre pode existir a chance de algo dar errado e em Go somos obrigados a realizar o handling desses erros. Porém, nossos commands não retornam mais nada além de error. Já nossas queries seguem o padrão de retornar dados.

O CQS é um conceito simples de ser seguido e pode parecer de pouco valor à primeira vista. No entanto, é importante refletir sobre o que poderia acontecer se fizéssemos o oposto do que é proposto por esse padrão.

  1. Anti-query: Um método de leitura de dados que insere uma entrada no banco de dados.
  2. Anti-command: Um método de escrita que retorna os dados escritos.

A anti-query é absurda, não faz o menor sentido que isso exista, já o anti-command é debatível. Na implementação do command, em alguns casos, é possível que você já possua o objeto de retorno imediatamente após a escrita, sem a necessidade de realizar uma operação de leitura adicional. Nesse cenário, é totalmente justificável retornar o objeto. No entanto, se você precisar fazer uma leitura logo após a escrita para obter o objeto e retornar os dados do repositório, estará transformando toda operação de escrita em uma operação de leitura também.

“Ah, mas eu preciso do objeto de retorno”

Se em 100% dos casos se fizer necessário retornar os dados manipulados do banco de dados, faz total sentido ignorar o command. Porém, se existir algum cenário em que o dado retornado é ignorado fora do repository, você estará realizando uma operação de leitura atoa.

Conclusões e Próximas Etapas

Nesse artigo foram apresentados dois conceitos importantes quando se trata de manipulação de dados, repository e CQS. Mas, como sempre em desenvolvimento de software, não existe bala de prata.

Adotar esses padrões tem seus benefícios e custos, caso você esteja trabalhando em uma aplicação simples, pode ser que não exista a necessidade de seguir esses padrões. Cada caso é um caso, é sempre bom tentar ser mais pragmático do que dogmático.

Agora que temos uma camada de manipulação de dados organizada, finalmente chegou o momento de integrar a API com um banco de dados, especificamente o MySQL. Para isso, utilizaremos o Docker e faremos a integração entre a API e o banco relacional por meio do docker-compose.

Referências

--

--

John Fercher

Tech lead at @MercadoLibre, gamer, master of science and open source contributor. More about: johnfercher.com