Precisamos falar de Injeção/Inversão de Dependência no Python

Cassio R. Eskelsen
6 min readAug 9, 2021

--

Fonte: stackify.com

Em outras linguagens, como o C# e o Java, esse assunto já está escrito na pedra faz muito tempo. No mundo Python ele costuma ser deixado de lado ou visto como algo que deve ser resolvido de modo mais “pythônico”.

Por outro lado, a característica de boa parte dos projetos feitos em Python não necessariamente exige a utilização desse Design Pattern: se, por exemplo, você precisa fazer um script para processar logs vindos do seu CDN e jogá-los no ElasticSearch você não irá se preocupar com abstrações e desacoplamento. KISS continua sendo o principal Design Pattern.

Quando falamos de aplicações mais complexas, no entanto, precisamos desacoplar os componentes, nem que seja apenas para possibilitar algo básico como testes unitários: você não irá querer subir uma instância de um banco de dados cada vez que for rodar seus testes. Enfatizo que possibilitar testes unitários é apenas um dos benefícios da DI/DIP.

Definição de DI

Injeção de Dependência (“Dependency Injection”) é uma técnica que permite que um objeto receba pronto todos os objetos dos quais ele depende. Se um serviço depende de um Repositório, ele receberá o repositório já instanciado.

Para entender melhor isso vamos para um exemplo prático: suponha que você tenha uma classe Customer com um método que retorna a lista de todos os clientes. Sem utilizar DI teríamos algo parecido com o código abaixo:

Da forma como amarramos o client do banco de dados no repositório precisaremos sempre ter um banco de dados Mongo no ar para os testes poderem ser executados. Estou ciente de que existem soluções como o mongomock mas nunca é uma implementação com 100% das funcionalidades que precisamos. Além disso, se o banco de dados utilizado fosse outro, não teríamos necessariamente uma versão fake do banco para utilizar em testes.

Outra situação em que o código acima poderá nos trazer problemas é quando chega o dia do “vamos trocar de banco”. Apesar de parecer um cenário muito improvável, ele pode acontecer.

Em termos de Arquitetura de Sistemas isso também está errado. Uma classe da camada de negócio não pode ter acoplamento com implementações da camada de infraestrutura. Isso fica bem claro no diagrama abaixo:

Para entender essa estrutura temos que ter na cabeça de que a camada mais de dentro sabe O QUE fazer com as informações, mas não deve saber COMO buscá-la.

Para resolver a comunicação entre as camadas utilizamos o famoso “D do SOLID”, ou seja Dependency Inversion Principle. O DIP nos diz que uma camada de mais alto nível não pode depender de uma camada de mais baixo nível. Ambas devem utilizar abstrações para a comunicação. Em linguagens como o Java e o C# isso é feito utilizando Interfaces mas como o Python não tem esse conceito, utilizaremos Classes Abstratas.

Para corrigirmos o código acima iremos abstrair a classe Repositório e criar implementações concretas para cada tipo de banco de dados que precisarmos (no caso, apenas MongoDB).

Com o intuito de tentar facilitar a compreensão, irei acrescentar uma classe “Service” que precisa fazer uso do Repositório para executar uma operação qualquer com todos os clientes.

Deixei todo o código em um mesmo .py mas no mundo real cada classe dessa estaria em um módulo diferente na nossa aplicação Python.

Definimos o nosso modelo Cliente nas linhas 8 a 11 na forma de uma Data Class.

Nas linhas 14–18 criamos o Contrato da dependência, ou seja, nossa “Interface”. Lembrando que em Python criamos uma Classe Abstrata a partir da metaclasse ABC. Esse contrato em particular define que o repositório terá um método para obter a lista de clientes e o retorno deve ser uma List de Customer.

Nas linhas 21–26 criamos uma implementação concreta para o repositório de clientes que sabe lidar com uma base guardada em um servidor MongoDB. Note que em uma aplicação real você não deverá deixar a string de conexão chumbada no código como eu fiz nesse exemplo.

As linhas 29–35 definem uma classe Service que faz algo com todos os clientes e precisa de uma lista deles. Perceba que ela não faz a mínima idéia de como efetivamente buscar essa lista, ela apenas declarou que precisa de um repositório de clientes e o código que a chamou que se vire para dizer de onde os dados vem! Os dados poderiam vir até de uma tabela em um legado em Cobol que o comportamento dela será o mesmo pois ela está esperando uma List de Customer.

A partir da linha 38 temos o ponto de entrada da aplicação. É a camada mais de fora do nosso círculo e ela é responsável por dizer qual classe concreta iremos usar. No caso, ela definirá que usaremos um banco de dados MongoDB.

Perceba que agora alcançamos o desejado desacoplamento entre as camadas. Poderíamos criar um outro Repositório simulando as operações no Mongo para utilizar em testes unitários e passaríamos ele como parâmetro para a classe CustomerService no lugar de MongoDBCustomerRepository.

Muitos provavelmente devem ter ficado incomodados com a instanciação das classes concretas no início do programa. E esse incômodo é compreensível pois além de parecer um tanto desorganizado nos remete à algumas questões como, por exemplo, como injetar uma dependência se a ordem de chamada dos métodos nem sempre é determinística.

Agora entramos em um tema no qual boa parte dos pythonistas torcem o nariz: a utilização de um container de IoC (alguns chamam de framework de IoC).

Conceituando, IoC (Inversion of Control) é um princípio de desenvolvimento de sistemas onde há uma “inversão” do controle do fluxo do programa: no lugar do programa determinar seu fluxo como estamos fazendo no entry point if __name__ == ‘__main__’ ele passa a ser controlado por um framework que chama módulos do sistema reagindo a eventos do sistema.
No entanto, o conceito de IoC é um tanto amplo, como muito bem pontuado nesse artigo. Vamos nos atentar aqui ao Container de IoC. O Container de IoC é responsável por injetar automaticamente as dependências de um objeto.

O Python possui alguns Containers de IoC disponíveis, tais como o python-inject, e o dependency-injector. Tenho preferido utilizar o Python-Inject pela sua simplicidade e eficiência. Confesso que nunca fiz testes mais profundos de performance e de carga para verificar qual se comportaria melhor, é algo que ficará para quando eu realmente sentir algum gap no python-inject.

Não irei fazer um tutorial muito detalhado da utilização do python-inject, na página dele existem vários exemplos de utilização. Irei apenas refatorar o código acima para utilizar o python-inject.

Começaremos instalando a dependência:

pip install inject

Segundo passo é fazer o registro (bind) das classes concretas com as “interfaces”:

Na Linha 12 chamamos o método de configuração do python-inject. Ele recebe como parâmetro uma função onde efetivamente iremos fazer a amarração das classes.

Deveremos chamar binder.bind para cada classe que iremos injetar. O primeiro parâmetro é a “interface” (classe abstrata) e o segunda é o objeto que iremos injetar. Sempre iremos injetar uma instância da classe concreta.

No exemplo leia como “sempre que alguém precisar de um CustomerRepository entregue essa instância de MongoDBCustomerRepository”.

É importante ressaltar que internamente o python-inject trabalha com Singletons então sempre entregará a mesma instância.

A utilização do objeto registrado acima será feita na classe CustomerService que precisa de um CustomerRepository, conforme exemplo abaixo:

A mágica está na linha 8. Chamamos o decorator autoparams do python-inject que irá analisar a assinatura do método/função. Caso um dos parâmetros seja alguma classe registrada em seu container, ele injetará automaticamente no método/função.

Caso você não utilize parâmetros tipados (mas deveria!!) pode fazer a injeção de forma mais declarativa @inject.param('customer_repository', CustomerRepository).

Nosso entrypoint ficará dessa forma:

Observe que chamamos CustomerService sem precisar passar o Repositório como parâmetro, o python-inject ficou responsável por isso.

O exemplo completo você pode clonar do github https://github.com/cassioeskelsen/precisamos-falar-de-di-ioc-python. Refatorei todas as classes iniciais para que o projeto ficasse um pouco mais parecido com uma aplicação do mundo real.

Inclui um pequeno teste apenas para demonstrar que podemos ter uma configuração diferente para eles. Para isso criei também um “InMemoryRepository”.

Concluindo

Espero ter conseguido transmitir o ganho que podemos ter utilizando Injeção de Dependência em nossas aplicações Python.

Sei que isso traz um pouco mais de trabalho inicial, mas os ganhos no médio prazo são enormes. Venho utilizando DI faz algum tempo em meus projetos Python e garanto que isso passa a ser normal. Você até estranha quando não está utilizando!

--

--