O princípio DRY ("não se repita") busca reduzir a duplicação de código e os problemas de manutenção resultantes, mas quando é mal aplicado aumenta o acoplamento e atrapalha a leitura do código. Conheça a opinião de vários especialistas sobre o princípio, suas aplicações e armadilhas.
DRY é uma abreviação para o inglês Don't Repeat Yourself, "Não se Repita". É o primeiro princípio do desenvolvimento de software mencionado por Andy Hunt e Dave Thomas no clássico livro O Programador Pragmático: de aprendiz a mestre. O princípio declara que:
Cada parte do conhecimento deve ter uma representação única, não ambígua e definitva dentro do sistema.
Nesse contexto, Hunt enfatiza o impacto negativo da duplicação e consequentemente a importância de usar DRY, no wiki do repositório de padrões de Portland:
A duplicação (seja ela acidental ou proposital) pode levar a pesadelos de manutenção, além de atrapalhar a refatoração e gerar contradições lógicas.
A duplicação e a possibilidade de contradições podem surgir em qualquer lugar: na arquitetura, em requisitos, código ou documentação. Os efeitos podem variar desde falhas de implementação de código, à confusão dos desenvolveres; até a falha completa do sistema.
Pode-se argumentar que a maior dificuldade na remediação do problema do Ano 2000, veio da falta de uma simples abstração de datas dentro de um sistema; o conhecimento das datas e o seu tratamento eram espalhado por muitas partes.
Embora o DRY pareça um princípio obrigatório na engenharia de software, Anders Munch alerta que há exceções:
Não há problema ter mais que uma representação para um pedaço de conhecimento, se houver um mecanismo eficaz que garanta a consistência entre os pedaços. Considere-se alguns casos:
- Definições e declarações de funções em C: estão geralmente sincronizadas, porque o compilador informa as inconsistências, forçando o programador a tomar alguma ação;
- Testes unitários: a presença de inconsistências significa que o teste irá falhar, novamente forçando que alguém tome alguma atitude;
- Código gerado automaticamente: a regeneração periódica garante a consistência.
Estas exceções na verdade reforçam as razões por trás do DRY, mas uma questão deve ser levada em conta: os programadores não estariam levando o princípio do DRY aos extremos? Não estaria o DRY sendo mal compreendido ou mal utilizado?
Quanto a isso, afirma Dave Thomas:
A maioria das pessoas interpretam o DRY como 'não duplique código'. Mas essa não é a intenção. A ideia por trás do DRY é muito mais ampla.
Thomas expande o princípio do DRY para todo um sistema de software:
O DRY diz que cada pedaço de conhecimento do sistema deve ter uma representação definitiva e não ambígua; que cada um desses pedaços no desenvolvimento de alguma coisa deve ter uma representação única. O conhecimento do sistema é muito mais abrangente que apenas o código. Inclui também esquemas de base de dados, planos de testes, o sistema de builds e até mesmo a documentação.
Dado todo esse conhecimento, porque devemos encontrar apenas uma maneira para representar cada característica? A resposta óbvia é que se há mais de uma maneira para expressar a mesma coisa, em algum momento haverá duas ou três representações diferentes que irão ficar inconsistentes entre si. Mesmo que não fiquem, haverá trabalho em manter as representações alinhadas sempre que uma alteração ocorrer. E mudanças irão ocorrer. O DRY é importante quando se quer software flexível e fácil de manter.
O problema é como representar todas estas diferentes partes de conhecimento somente uma vez. Se for apenas código, então se pode obviamente organizá-lo para que não se repita, com ajuda de métodos e subrotinas. Mas como tratar de coisas como esquemas de base de dados? É aí que se aplicam outras técnicas do livro [Pragmatic Programmer], como o uso de ferramentas de geração de código, sistemas de automação de builds e linguagens de script. Estas técnicas permitem representações únicas e definitivas que irão gerar produtos de trabalho não-definitivos como códigos ou DDLs (linguagem de descrição de dados).
Apesar de as questões sobre o uso do DRY parecerem ter sido resolvidas há um bom tempo, o princípio foi mencionado novamente no QCon 2012 de Londres por vários palestrantes, incluindo Greg Young e Dan North. Este último chamou atenção para possíveis erros na sua aplicação. Ouvimos Greg Young sobre os problemas do DRY:
O argumento básico contra seguir o DRY é que há um outro lado da questão. Quando seguimos o princípio, é muito comum que desenvolvedores comecem a adicionar acoplamentos e complexidade aos seus softwares. Um lado é muito mais fácil de medir: a quantidade de tentativas por hora de editar/compilar/testar, quando é preciso corrigir bugs em muitos locais; já o outro lado é mais difícil de mensurar: o acoplamento e a complexidade embutidos no software em nome do DRY.
Pode-se argumentar que se o DRY for seguido "corretamente" nunca haverá acoplamento e complexidade dentro do software. Isto é até possível; posso escrever códigos que seguem perfeitamente o DRY, sem introduzir acoplamento e complexidade. Mas isso pressupõe que meu conhecimento seja perfeito.
O Infoq.com também ouviu David Chelimsky, autor e líder de desenvolvimento do RSpec. Ele afirma que viu o DRY sendo usado até o "nível de linha de código, o que nem sempre é apropriado (embora algumas vezes possa ser usado assim)". Ele apresenta o seguinte exemplo:
describe "Person#full_name" do it "concats the first and last names" do first_name = "John" last_name = "Doe" person = Person.new(:first_name => first_name, :last_name => last_name) person.full_name.should eq "#{first_name} #{last_name}" end end
Embora este pedaço de código evite duplicações e possa parecer uma boa implementação do DRY, Chelimsky prefere código mais legível:
describe "Person#full_name" do it "concats the first and last names" do person = Person.new(:first_name => "John", :last_name => "Doe") person.full_name.should eq "John Doe" end end
E acrescenta:
Uma pessoa que não entende o DRY e acha que o princípio deve ser obedecido religiosamente, pode estranhar ver "John" e "Doe" duas vezes neste último exemplo. Para mim é o contrário. Na versão com repetição, fica mais fácil visualizar o relacionamento entre o primeiro e o último nomes e a saída no full_name.
Chelimsky também destaca outro trecho de código que encontrou recentemente no framework Objectify.
1 def request_resolver 2 klass = Objectify::NamedValueResolverLocator 3 @request_resolver ||= klass.new.tap do |resolver 4 resolver.add(:controller, self) 5 resolver.add(:params, params) 6 resolver.add(:session, session) 7 resolver.add(:cookies, cookies) 8 resolver.add(:request, request) 9 resolver.add(:response, response) 10 resolver.add(:flash, flash) 11 resolver.add(:renderer, Renderer.new(self)) 12 end 13 end
Nele, as linhas 3 a 11 foram substituídas por:
{:controller => self, :params => params, :session => session, :cookies => cookies, :request => request, :response => response, :flash => flash, :renderer => Renderer.new(self) }.each do |key, value| resolver.add(key, value) end
Sobre a alteração, diz Chelimsky: "Na minha opinião, o DRY está sendo levado longe demais. O trecho era mais fácil de manter antes da alteração".
Segundo Chelimsky "as palavras 'Não se repita' deviam funcionar apenas como um lembrete rápido, mas acabaram se tornando um princípio geral para muitas pessoas". Ele destaca o acoplamento gerado pelo uso excessivo:
Quando dois métodos do mesmo objeto fazem as mesmas coisas, tipicamente extraímos um terceiro método que ambos os métodos originais irão chamar. Com isso, os métodos originais ficam acoplados ao método extraído e indiretamente um ao outro. Isto parece lógico e inofensivo no contexto de um único objeto, mas e quando reconhecemos comportamentos similares entre dois ou mais objetos?
Para eliminar a duplicação, temos que introduzir um novo objeto, que será uma dependência dos dois objetos originais; ou ainda pior (e mais comum), fazer um dos objetos originais depender do outro. Essa abordagem cria dependências artificiais entre objetos não-relacionados, reduzindo sua capacidade de evoluir ao longo do tempo. Introduzir um novo objeto aumenta consideravelmente a "superfície" do sistema, e deve ser feito com cuidado e reflexão, o que nem sempre ocorre em refatorações.
Para evitar o uso extremo do DRY, Chelimsky propõe balancear seu uso com o de outros princípios de desenvolvimento:
O DRY é importante, mas os princípios SOLID de Uncle Bob, por exemplo, ou outros conceitos mais amplos como baixo acoplamento e alta coesão, são igualmente importantes. Não é suficiente apenas aplicar um princípio o tempo todo; deve-se levar em conta todos os princípios e pesar seus valores relativos de acordo com a situação. É como saber qual tempero deve ser colocado no peixe e qual adicionar na carne. Alguns funcionam nos dois casos; outros nem tanto.