16 March 2012

Quem me conhece sabe que eu gosto bastante de linguagens com tipagem estática, mas vou parar um pouco para falar sobre acoplamento em linguagens com tipagem dinâmica e como isso é diferente do que eu estou acostumado a fazer normalmente.

Vou começar por uma execução de um método qualquer em ruby:

class MinhaClasse
  def faz_alguma_coisa(outro_cara)
    outro_cara.algum_metodo
  end
end

Qual é o nível de acoplamento da MinhaClasse com o outro_cara? O mais baixo possível! Só existe o acoplamento com a interface, no sentido mais puro da palavra, ou seja, só precisamos que o outro_cara tenha um método chamado algum_metodo, que não recebe nenhum argumento. Ou seja, o baixo acoplamento vem de graça! Não existe o acoplamento de tipos, que existe nas linguagens estáticas.

Se existirem duas classes que tem esse método, qualquer uma delas pode ser usada:

class CaraLegal
  def algum_metodo
    puts "yey!"
  end

  def outro_metodo
    puts "=)"
  end
end

class CaraChato
  def algum_metodo
    puts "Zzzzz"
  end
end

Agora se mudar um pouco a MinhaClasse, por exemplo:

class MinhaClasse
  def faz_alguma_coisa(outro_cara)
    outro_cara.algum_metodo

    outro_cara.outro_metodo
  end
end

Quebra todos os lugares que usam o CaraChato ou qualquer outra classe do sistema como argumento nesse método (inclusive os mocks que não esperavam a mensagem :‍outro_method ¬¬). Quanto mais métodos eu chamar no outro_cara, mais eu estou me acoplando à interface dele, e maior é a chance de quebrar algum código que usa a MinhaClasse. Ou seja, ao mesmo tempo em que eu estou me acoplando pouco a um objeto, estou me acoplando a todas as classes que possuem os métodos que chamei em faz_alguma_coisa.

Agora, suponha que a MinhaClasse precisa enviar um email, e eu resolvi implementar da seguinte maneira:

class MinhaClasse
  #...
  def processo_complicado
     email = gera_email 
     enviador = EnviadorDeEmail.new
     enviador.envia email
  end
end

Agora além de estar se acoplando a uma interface, estaria também se acoplando a uma implementação! A vida me ensinou que nesse caso, para remover esse acoplamento posso usar injeção de dependências e receber esse enviador no construtor da MinhaClasse, por exemplo. Mas peraí, isso é Ruby, será que esse código está acoplado à implementação do EnviadorDeEmail mesmo? Se eu quiser trocar a implementação padrão por uma que não fica esperando o email ser enviado, por exemplo, vou ter que varrer o sistema inteiro pra fazer isso? Não. Só preciso fazer isso em algum lugar:

EnviadorDeEmail = EnviadorDeEmailNaoBlocante

Se eu tenho várias classes que oferecem serviços e são usadas como dependências no meu sistema, basta eu escolher um nome para o serviço, e dar new na hora que eu precisar usar (ou coisa parecida). E no meu ponto de entrada do sistema, simplesmente faço o 'bind' desses serviços para as implementações:

EnviadorDeEmail = EnviadorDeEmailNaoBlocante
Cache = MemCache
GeradorDeNotaFiscal = NotaFiscal::GeradorViaSoap
#...

Sem contar que conseguimos implementar o new pra retornar a mesma instância, já que ele é um simples método da classe. Os motivos que tínhamos para não dar new nas classes não valem aqui (pelo menos não a maioria deles). O problema é: qual é o perigo disso?



  • - Alberto Souza - Fri, 16 Mar 2012 08:53:54 -0700
    Fiquei com um pouco de medo :). A solução é bem esperta, como é de praxe no seu caso. Para o caso do Mock eu acho que pode ser um pouco pior. Acho que se você estubar aquele método, nos seus testes vai parecer que aquela instancia tem sim ele... Mas na hora que você rodar vai dar caca. É o problema da alta flexibilidade de mexer nas classes :). Para mim, usar a constante ali me deixa preocupado com a legibilidade. O primeiro pensamento da pessoa, pelo menos na maioria dos casos, vai ser o de procurar uma classe com o nome EnviadorDeEmail e ela vai ter que descobrir que isso é só uma variavel....
  • - Leonardo - Fri, 16 Mar 2012 09:10:24 -0700
    Olá Lucas, muito bom o post. Um approach que acho bacana sobre injeção de dependências em Ruby, e fornecer um o serviço no próprio método e deixar como padrão algum serviço Por exemplo: class MinhaClasse #... def processo_complicado(enviador = EnviadorDeEmail.new) email = gera_email enviador.envia email end end Neste caso eu poderia chamar o metodo processo_complicado sem passar nenhum argumento e ele usaria o serviço EnviadorDeEmail por padrão, ou então caso eu desejasse uma outra implementação eu poderia chamar o metodo processo_complicado passando esse novo serviço: processo_complicado EnviadorDeEmailNaoBlocante.new Abraços, Leonardo
  • - Rafael Ponte - Fri, 16 Mar 2012 09:51:06 -0700
    Provavelmente não podemos considerar como "perigo", mas no final temos um objeto com uma dependência não explicita. Outro ponto, será que neste seu cenário a classe EnviadorDeEmail atuaria como uma variável global, não? Se for o caso, variáveis globais costumam trazer mais problemas do que gostaríamos.
  • - Lucas Cavalcanti - Fri, 16 Mar 2012 09:54:37 -0700
    Você pode pensar nessa variável como se fosse uma interface do java. Acontece a mesma coisa, vc vai procurar a classe, daí é uma interface e vc precisa procurar a implementação... se você fizer algo do tipo:
    # deve ter o metodo:
    # envia(email)
    EnviadorDeEmail = ....
    
    vc criou uma "interface" em ruby.
  • - Lucas Cavalcanti - Fri, 16 Mar 2012 10:08:18 -0700
    Todas as classes são variáveis globais em ruby ;) Na verdade todas as dependências estão explicitas na implementação (vc quem declarou com os news quais são elas), e escondidas no uso da classe, o que é o melhor dos casos, já que quem usa o EnviadorDeEmail não precisa saber quais são as dependências dele
  • - Pedro Matiello - Fri, 16 Mar 2012 10:22:06 -0700
    > Todas as classes são variáveis globais em ruby ;) Mas quão normal é fazer algo assim em Ruby: "AlgoQuePareceNomeDeUmaClasse = NomeDeUmaClasseDeVerdade"? Se isso não é muito comum, a classe é global mas não corre um grande risco de ser reatribuída. Ainda: e se eu quiser usar duas implementações diferentes (i.e. tenho EnviadorDeEmailTextoPuro e EnviadorDeEmailHtml) e preciso escolher qual vou usar no momento do uso, e não na inicialização da aplicação?
  • - Rafael Ponte - Fri, 16 Mar 2012 11:18:27 -0700
    Bem, não podendo haver DI através de construtor ou setter eu normalmente não considero uma dependência explicita. Isso dificulta mockar (pra ruby nem tanto), mas ainda assim eu estava me referindo as DI na MinhaClasse.
  • - Lucas Cavalcanti - Fri, 16 Mar 2012 11:28:59 -0700
    Não sei o quão normal é... Acho que quase não é usado. se tem duas implementações a serem usadas simultaneamente não dá pra usar isso... mas em java com container de DI tb não funciona ;)
  • - Lucas Cavalcanti - Fri, 16 Mar 2012 11:31:39 -0700
    A idéia era que, se eu precisar do enviador de emails na Minha classe eu simplesmente faço um EnviadorDeEmail.new... é explicita ;) a configuração vai definir qual é a implementação. "Injeção de Dependência" via sobrescrita de "constante" de tipo ;)
  • - Pedro Matiello - Fri, 16 Mar 2012 12:57:29 -0700
    Mas ainda existe a possibilidade de construir o objeto manualmente se a injeção é por construtor. Ainda, no exemplo do post, eu diria que a dependência não é realmente injetada, mas buscada pela classe cliente. Comparado com o código similar em Java, há a vantagem de ser fácil configurar o resultado da busca para ambientes diferentes com as atribuições no ponto de entrada da aplicação (em Java a gente usaria uma factory e um monte de código entediante ou algo do tipo). E também é muito mais fácil testar em Ruby porque o RSpec tem grandes poderes.
  • - Rafael de F. Ferreira - Fri, 16 Mar 2012 15:05:42 -0700
    Esse é o grande problema dos containers de DI. Reduzem a flexibilidade do modelo OO à de módulos estáticos tipo C ou ML. Se a escolha da dependência é global, não há grande vantagem em relação à dependência direta da implementação.