Back-End

22 dez, 2015

Injeção de Dependência em Java e Python

Publicidade

Este artigo foi publicado em 10/02/2015. Por ter sido considerado um dos melhores artigos de 2015, foi republicado hoje.

Aprendi Orientação a Objetos em Java. Inicialmente me foi ensinado a utilizar o construtor para inicializar todas dependências que o objeto necessita para funcionar.  Sendo assim, o código da classe Sorteador é perfeitamente válido:

package di;
 
public class Sorteador {
 private long limite;
 
 public Sorteador(long limite) {
 this.limite = limite;
 }
 
 public long sortear(){
 double numero = Math.random()*this.limite;
 return Math.round(numero);
 }
 
}

Contudo, o problema dessa abordagem consiste na dificuldade de testar o método sortear. Como ele depende de um número aleatório, o máximo que se consegue é verificar se o sorteio se encontra dentro dos limites esperados, conforme código SorteadorTest:

package di;
 
import static org.junit.Assert.*;
 
import org.junit.Test;
 
public class SorteadorTest {
 
 @Test
 public void testSortear() {
 Sorteador sorteador=new Sorteador(10);
 long sorteio=sorteador.sortear();
 assertTrue(sorteio>=0);
 assertTrue(sorteio<=10);
 }
 
}

Para contornar o problema, poderíamos componentizar com a finalidade de alterar o comportamento no momento de teste. Com esse objetivo, criei a interface Randomizador:

package di2;
 
public interface Randomizador {
 double random();
 
}

Para continuar executando o comportamento original, criei a classe RandomizadorReal:

package di2;
 
public class RandomizadorReal implements Randomizador {
 
 @Override
 public double random() {
 return Math.random();
 }
 
}

Dessa maneira alterei o código do Sorteador . Com essa nova abordagem é possível alterar o componente de randomização utilizando o construtor sobrecarregado. Nessa caso, a dependência do objeto é injetada de fora para dentro. Por isso esse conceito é chamado de “Inversão de Controle” ou também “Injeção de Dependência”:

package di2;
 
public class Sorteador {
 private long limite;
 private Randomizador randomizador;
 
 public Sorteador(long limite, Randomizador randomizador) {
 super();
 this.limite = limite;
 this.randomizador = randomizador;
 }
 
 public Sorteador(long limite) {
 this(limite, new RandomizadorReal());
 }
 
 public long sortear(){
 double numero = randomizador.random()*this.limite;
 return Math.round(numero);
 }
 
}

Com o código modificado é possível escrever um teste com mais controle. Para isso, basta trocar o componente RandomizadorReal pela classe RandomizadorMock, conforme código a seguir:

package di2;
 
import static org.junit.Assert.*;
import org.junit.Test;
 
class RandomizadorMock implements Randomizador{
 public double retorno=0;
 @Override
 public double random() {
 return retorno;
 }
 
}
 
public class SorteadorTest {
 
 @Test
 public void testSortear() {
 RandomizadorMock mock=new RandomizadorMock();
 Sorteador sorteador=new Sorteador(10, mock);
 mock.retorno=0;
 long sorteio=sorteador.sortear();
 assertEquals(0, sorteio);
 mock.retorno=0.5;
 sorteio=sorteador.sortear();
 assertEquals(5, sorteio);
 mock.retorno=0.54;
 sorteio=sorteador.sortear();
 assertEquals(5, sorteio);
 mock.retorno=0.56;
 sorteio=sorteador.sortear();
 assertEquals(6, sorteio);
 
 }
 
}

Esses objetos que se utilizamos para substituir componentes na hora de testes costumam ser chamados de Stub ou Mock. Foi com esses conceitos em mente que migrei para a linguagem Python.

Nela posso escrever classe similar a primeira versão de Sorteador:

from random import random 
 
class Sorteador():
 def __init__(self, limite):
 self.limite = limite
 
 def sortear(self):
 return round(self.limite * random())

Poderia empregar a mesma técnica para componentizar o método sorteio. Contudo, Python tem uma particularidade interessante: tudo, absolutamente tudo, é um objeto. Inclusive as bibliotecas importadas em um módulo.

Para provar isso, é muito simples. Basta chamar a função dir para inspecionar os elementos do módulo di.py:

>>> import di
>>> [elemento for elemento in dir(di) if not elemento.startswith('__')]
['Sorteador', 'random']

Assim, é possível observar a função random importada como sendo um de seus elementos. Ou seja, bibliotecas importadas em um módulo funcionam como variáveis. Sendo assim, em vez de injetar dependência, a biblioteca alvo pode ser simplesmente alterada na hora do teste:

from unittest import TestCase
import di
 
class SorteadorTestes(TestCase):
 def teste_sortear(self):
 sorteador = di.Sorteador(10)
 di.random = lambda: 0.5
 sorteio = sorteador.sortear()
 self.assertEqual(5, sorteio)
 
 di.random = lambda: 0.54
 sorteio = sorteador.sortear()
 self.assertEqual(5, sorteio)
 
 di.random = lambda: 0.56
 sorteio = sorteador.sortear()
 self.assertEqual(6, sorteio)

De início passei a utilizar essa técnica mas achava que estava fazendo gambiarra. No entanto descobri a biblioteca Mock do Python. Sua função patch serve exatamente para isso, trocar uma biblioteca em tempo de teste por outra. E ela ainda vai além, tomando o cuidado de retornar o componente para seu objeto original após o teste, evitando assim efeitos colaterais indesejados em outros testes.

Enfim, eu gostei muito dessa abordagem distinta do Python. E você o que acha? É gambiarra ou apenas uma forma prática de se fazer Injeção de Dependência?