Technical
Como gerir as implicações incómodas de mudanças nas fontes de dados
Rafael Cavalheiro · 20 de novembro de 2022
Discutamos um cenário comum numa consultoria de IA. O cliente fornece acesso a fontes de dados em formatos como CSVs ou bases de dados que não estão num ambiente de produção. Porquê? Geralmente, estão a explorar o valor do projeto, não querem divulgar demasiados dados e pretendem evitar problemas técnicos nas fases iniciais. Compreendemos isso!
Depois, validamos a ideia e desenvolvemos uma solução que resolve com sucesso o desafio em pilotos internos. Agora, o cliente está entusiasmado e quer colocá-la em produção… oh, não! É necessário alterar todo o código para se adequar ao novo ambiente.
Como podemos evitar que isto aconteça? É aqui que o Repository Pattern se torna realmente útil, ajudando-nos a poupar tempo e esforço.
Repository Pattern – O que é?
O Repository Pattern é uma forma bem pensada e documentada de lidar com fontes de dados.
Consiste numa abstração sobre armazenamento persistente, ocultando os detalhes entediantes do acesso a dados, fingindo que todos os nossos dados estão em memória.
Como o nome nos diz, a personagem principal deste padrão de desenho é o repositório. Os repositórios são um conjunto de objetos e classes que nos ajudam a encapsular a lógica necessária para aceder às fontes de dados.
Os repositórios situam-se entre o modelo de domínio e as fontes de dados, permitindo-nos desacoplar a camada de modelo da camada de dados.
Que problemas o Repository Pattern ajuda a resolver?
Consideremos um cenário em que temos um site para uma loja online, com várias páginas. Uma página pode ser a página de Produtos e outra a página As Minhas Encomendas. Em ambas as páginas, existe um cabeçalho que cumprimenta o utilizador com uma caixa de texto dizendo "Bem-vindo João Silva!".
Tendo em conta a solução atual, analisemos os problemas deste desenho:
O primeiro problema é o facto de tanto MyOrdersController como ProductsController implementarem o seu método privado para obter o utilizador. Em programação, isto chama-se duplicação de código que viola o princípio DRY (Don't Repeat Yourself). Ao fazer isto, estamos a adicionar complexidade ao nosso código, o que torna mais difícil fazer alterações, o que leva a gastar mais tempo e esforço na manutenção do código.
Por exemplo, imaginemos que precisamos de alterar a forma como acedemos ao User (por exemplo, mudança de BD). A solução atual requer alterações em ambos os métodos, o que significa que gastamos mais tempo analisando o impacto de uma mudança de BD, uma vez que existe mais de um local no código onde precisamos de olhar.
O segundo problema com este desenho é que estamos a pedir aos controladores para gerir e compreender como aceder aos dados.
Neste caso, uma vez que os dados estão armazenados numa Base de Dados, isto significa que os controladores são responsáveis por gerir coisas como ligações de BD e cadeias de ligação.
Isto significa que temos uma arquitetura fortemente acoplada, uma vez que estamos a ligar os nossos controladores à infraestrutura utilizada para armazenar os nossos dados, o que se traduz em código menos flexível e difícil de manter.
O terceiro problema é o facto de o código ser difícil de testar. Imaginemos que a nossa fonte de dados é uma base de dados SQL Server. Se quiséssemos testar o nosso código, precisaríamos de configurar o SQL Server, obter a cadeia de ligação, configurar o Entity Framework, gerar a Base de Dados de Teste, inserir os dados e apenas então poderíamos testar o código, validar os erros e agir sobre esses erros. Esta cadeia de ações não é prática de todo, e torna difícil escrever código limpo e testável.
Como implementar o Repository Pattern?
Passemos pela forma como podemos utilizar o Repository Pattern para melhorar o nosso desenho.
Como descrito no início deste artigo, o Repository Pattern trata-se de abstrair as fontes de dados da camada de aplicação. Que benefícios traz este desenho?
Em primeiro lugar, pode ver-se que toda a lógica relacionada com o acesso aos dados está agora centralizada num User Repository e deixámos de ter problemas de duplicação de código. Desta forma, o nosso código está a respeitar o princípio SOLID do DRY. Ambos os nossos controladores estão a chamar o mesmo método de um repositório único para obter a informação do utilizador. Com esta abordagem, o nosso código é melhor mantido uma vez que estamos a simplificar a nossa arquitetura, tornando-a mais flexível e mais fácil de alterar.
Outro benefício é que a camada de aplicação está agora desacoplada da camada de infraestrutura. Isso nos dá a possibilidade de alterar ambas as camadas de forma independente.
O que também fizemos foi alterar a responsabilidade de interagir com a fonte de dados dos controladores para o repositório. Isto permite-nos utilizar interfaces para definir o comportamento que os nossos controladores podem esperar do repositório e, desta forma, encapsulamos os detalhes de implementação da fonte de dados. Com estas melhorias, ganhamos flexibilidade e escalabilidade ao considerar opções de desenho sobre como aceder aos dados. Portanto, se precisarmos de alterar a forma como acedemos aos nossos dados (API, NOSQL, ficheiro CSV, etc), apenas precisamos de alterar o repositório, sem nos preocuparmos em quebrar a camada de aplicação.
Quais são as 3 melhores práticas ao implementar o Repository Pattern?
Passemos por algumas das melhores práticas ao implementar o Repository Pattern.
Implementar Operações CRUD
Uma boa prática é implementar operações CRUD no repositório. Sempre que estamos a implementar o Repository Pattern, devemos sempre tentar desenhar os repositórios para trabalhar com operações CRUD. Isto facilita a forma como interagimos com o repositório. Por exemplo, no desenho anterior no User Repository, poderíamos ter métodos como:
- get_user(id)
- list_users(**filters)
- add_user(**kwargs)
- update_user(**kwargs)
- delete_user(id)
Um Repositório por Objeto de Negócio
Outra boa prática é ter um repositório por objeto de negócio.
O Single Responsibility Principle diz que "cada classe deve ter apenas uma razão para mudar", portanto, tendo isto em consideração, faz sentido que tenhamos um repositório para cada objeto de negócio nos nossos dados. Por exemplo, se temos dados sobre utilizadores, produtos e encomendas, devemos implementar 3 repositórios:
Fornecer um Contrato ou Interface
Como pode notar na figura acima, para cada repositório existe um repositório abstrato, que é uma classe abstrata. Uma classe abstrata é uma classe que não pode ser instanciada. No entanto, podemos criar outras classes que herdam as propriedades da classe abstrata. Pode pensar na classe abstrata como um esquema ou um contrato, onde definimos todos os métodos que uma classe filha irá herdar. O que isto significa é que a classe abstrata não terá qualquer implementação de lógica, mas apenas a estrutura da classe. Essa lógica será implementada numa classe filha que herda da classe abstrata.
Porquê é isto uma boa prática? Bem, isto ajuda-nos a manter uma arquitetura vagamente acoplada, contribuindo para a abstração da última camada onde temos as nossas ligações de fonte de dados. Desta forma, não precisamos de injetar diretamente classes concretas nos nossos controladores, passamos apenas uma interface (a classe abstrata), para que os controladores saibam o que esperar da camada de infraestrutura.
Isto permite-nos utilizar diferentes implementações da mesma interface dentro da nossa arquitetura. Um exemplo muito bom é, para fins de teste, apenas precisamos de substituir a base de dados com dados em memória como um retorno de uma lista ou um array, por exemplo, sem ter necessidade de alterar a forma como os controladores interagem com a interface.
Conclusões
Como detalhado neste artigo, o Repository Pattern pode ser transformador para programadores ou cientistas de dados. Quer desenvolvendo uma solução para um cliente, quando frequentemente não temos a fonte de dados de produção nas fases iniciais, ou desenvolvendo um POC para um produto usando dados simulados, o Repository Pattern poupa-nos tempo e esforço a longo prazo e torna o nosso código mais limpo e mais fácil de manter, por ter uma arquitetura vagamente acoplada, permitindo-nos alterar as camadas de aplicação e infraestrutura de forma independente.
