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?
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....
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
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.
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: vc criou uma "interface" em ruby.
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
> 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?
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.
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 ;)
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 ;)
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.
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.