Ir para o conteúdo principal
Background Image

DRY é mais do que código

··6 minutos·
Rafael Issao
Autor
Rafael Issao
Apaixonado por tecnologia, programação e inovação. Adoro compartilhar conhecimento e aprender coisas novas todos os dias.
Tabela de conteúdos

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! 🥒🥒