Quando falamos de facilidade de manutenção e entregar um software confiável para o cliente, um dos princípios que surgem como uma ferramenta interessante é o DRY ou Don’t Repeat Yourself:
Todo conhecimento deve ter uma representação única, inequívoca e autoritativa dentro do sistema.
Mas aplicar o DRY é mais difícil e complexo do que parece.
Neste artigo vamos ver exemplos de aplicação do DRY em vários contextos e estender a aplicação do princípio para fora do código.
📝 Nota
Este artigo é baseado no livro The Pragmatic Programmer. É o livro dos autores que criaram o princípio DRY. Recomendo!
Antes dos exemplos…#
Vamos deixar uma coisa clara sobre este princípio.
Ela é sobre código duplicado, mas isto é só uma pequena parte.
É sobre duplicar conhecimento.
Quando duplicamos o conhecimento, a consequência é que duplicamos ou triplicamos o esforço para alterá-lo. Assim aumenta o custo do software desnecessariamente.
Alguns exemplos da quebra do DRY:
- Quando adiciona um campo novo no objeto, você tem que alterar também em outros lugares do seu código
- Pra corrigir o mesmo bug, tem que corrigir em mais de um lugar
- Quando altera o código, tem que alterar a documentação dela também
Quando quebramos o DRY, o esforço para alterar o código é maior.
O exemplo mais simples#
Veja o código abaixo:
Pessoa pessoa = Pessoa.comNome("Rafael", "Issao");
imprimeNoArquivo(pessoa.nome() + " " + pessoa.sobrenome());
imprimeNoTerminal(pessoa.nome() + " " + pessoa.sobrenome());
enviaViaRest(pessoa.nome() + " " + pessoa.sobrenome());
Claramente temos um código duplicado mas ele é um conhecimento duplicado?
Sim!
O que estamos passando no parâmetro de cada função é o nome completo da pessoa.
Ou seja, existem três representações do nome completo no nosso código. Estamos quebrando o DRY. Vamos refatorar:
class Pessoa {
String nome
String sobrenome
// Static factory method
static Pessoa comNome(String nome, String sobrenome) {
return new Pessoa(nome: nome, sobrenome: sobrenome)
}
// Unica representacao e autoritativa do nome completo.
def nomeCompleto() {
return "${nome} ${sobrenome}"
}
}
Pessoa pessoa = Pessoa.comNome("Rafael", "Issao")
imprimeNoArquivo(pessoa.nomeCompleto())
imprimeNoTerminal(pessoa.nomeCompleto())
enviaViaRest(pessoa.nomeCompleto())
Com o novo design, se quisermos alterar algo sobre o nome completo, precisamos somente alterar no método Pessoa#nomeCompleto
Mas nem sempre código duplicado significa conhecimento duplicado.
Exemplo de código duplicado mas sem quebra do DRY#
def validaPeso(valor) {
deveSerDouble(valor)
deveSerMaiorQue(0.0, valor)
}
def validaAltura(valor) {
deveSerDouble(valor)
deveSerMaiorQue(0.0, valor)
}
Esse é um exemplo onde o código está duplicado mas representa conhecimentos diferentes.
Um valida o peso e outro valida a altura e, por coincidência, eles são iguais. Mas no futuro, cada uma destas funções podem evoluir de forma diferente.
Códigos de teste tem a tendência de ter, por coincidência, códigos duplicados mas que não representa conhecimento duplicado:
class ExerciseNameTest {
@Test
void "O titulo nao deve ter espacos em volta"() {
ExerciseName exerciseName = ExerciseName.with(" Agachamento ")
assertThat(exerciseName.fullTitle)
.isEqualTo("Agachamento")
}
@Test
void "O nome do arquivo nao deve ter espacos em volta e deve comecar com minusculo"() {
ExerciseName exerciseName = ExerciseName.with(" Agachamento ")
assertThat(exerciseName.buildVideoFileName())
.isEqualTo("agachamento")
}
}
Estamos testando comportamentos diferentes em cada teste.
Um teste verifica como o título deve funcionar e o outro teste verifica como deve ser o nome do arquivo.
Exemplo com documentação#
Comentar códigos pode levar a quebra do DRY, por exemplo:
/**
* Função para calcular o imposto de renda no Brasil com base na renda bruta mensal.
*
* Tabela de alíquotas (2024):
* - Até R$ 2.112,00: Isento
* - De R$ 2.112,01 a R$ 2.826,65: 7,5% (dedução: R$ 158,40)
* - De R$ 2.826,66 a R$ 3.751,05: 15% (dedução: R$ 370,40)
* - De R$ 3.751,06 a R$ 4.664,68: 22,5% (dedução: R$ 651,73)
* - Acima de R$ 4.664,68: 27,5% (dedução: R$ 884,96)
*
* @param rendaBruta Renda bruta mensal (double)
* @return O valor do imposto de renda (double)
*/
def calcularImpostoDeRenda(rendaBruta) {
if (rendaBruta <= 2112.00) {
return 0.0
} else if (rendaBruta <= 2826.65) {
return rendaBruta * 0.075 - 158.40
} else if (rendaBruta <= 3751.05) {
return rendaBruta * 0.15 - 370.40
} else if (rendaBruta <= 4664.68) {
return rendaBruta * 0.225 - 651.73
} else {
return rendaBruta * 0.275 - 884.96
}
}
Aqui temos dois lugares que representam o conhecimento sobre como calcular o imposto de renda.
Se mudarmos o comportamento em um lugar, devemos alterar no código e no comentário.
Para manter o DRY, devemos utilizar algumas técnicas de refatoração para tornar o código mais legível para o leitor:
def calculaImpostoDeRendaBrasil2024(rendaBruta) {
def tabelaImpostoRenda = com(
faixa(inferior: 0.00, superior: 2112.00, aliquota: 0, deducao: 0.00),
faixa(inferior: 2112.01, superior: 2826.65, aliquota: 7.5, deducao: 158.40),
faixa(inferior: 2826.66, superior: 3751.05, aliquota: 15, deducao: 370.40),
faixa(inferior: 3751.06, superior: 4664.68, aliquota: 22.5, deducao: 651.73),
faixa(inferior: 4664.69, superior: MAX_VALUE, aliquota: 27.5, deducao: 884.96)
)
def faixa = tabelaImpostoRenda.encontraFaixa(rendaBruta)
return rendaBruta * faixa.aliquota - faixa.deducao
}
Deixar o código legível é uma das maneiras de evitar comentários redundantes e duplicados.
Violação do DRY nos dados#
Este exemplo é retirado diretamente do livro.
É possível quebrar o princípio DRY quando representamos o conhecimento na estrutura de dados.
Por exemplo:
class Linha {
Ponto inicio
Ponto fim
double comprimento
}
É uma classe que representa a linha que possui dois pontos, início e fim, e o comprimento da linha.
Mas quebramos o DRY. Se mudarmos o início ou o fim, temos que lembrar de mudar o comprimento também.
Para não quebrar o DRY neste exemplo, podemos calcular o comprimento sempre a partir do início e fim:
class LinhaRefatorada {
Ponto inicio
Ponto fim
def comprimento() {
return inicio.distancia(fim)
}
}
Neste caso, a representação única, inequívoca e autoritativa são os pontos. O comprimento é um conhecimento derivado destes pontos.
Conclusão#
Para facilitar a compreensão sobre DRY, vimos que ele se trata de duplicação de conhecimento.
Se tiver mais de um lugar que representa o mesmo conhecimento, teremos o esforço duplicado para mantê-lo atualizado em todos os lugares.
E normalmente isso gera problemas como:
- Mesmos bugs se repetindo no sistema
- Problemas de documentação desatualizada
- Código reaproveitado mal feito que gera mais manutenção do que efetivamente diminuir
Isso gera custo para o sistema e para a sua empresa.
Mas uma coisa importante: quebrar o princípio não é ruim por si só!
O mais importante é saber quando não quebrar o DRY. E se quebrar, aceitar as consequências destas decisões!
Você aceita o custo de quebrar o DRY ou não?
Se você tiver dúvidas, sugestões ou até correções, sinta-se à vontade para comentar ou falar diretamente comigo!
🥒🥒 Até o próximo artigo! 🥒🥒


