Por que o princípio Don't Repeat Yourself é tão importante?

DRY nunca foi sobre código. É sobre conhecimento. É sobre coesão.

Por que o princípio Don't Repeat Yourself é tão importante?

Não é a primeira vez e não será a última que comento sobre esse princípio que envolve o desenvolvimento de software. Em outro artigo abordo o Don't Repeat Yourself e passamos por alguns exemplos em código. O objetivo deste artigo é mostrar que desenvolver software requer atenção e uma analise bem mais profunda, principalmente quando temos que criar novas funcionalidades que foram solicitadas. Vamos abordar a importância do DRY e responder essas perguntas: Quais os fatores que podem levar a duplicidade de conhecimento em uma aplicação? Qual o risco de duplicar esse conhecimento? Como podemos evitar duplicar o conhecimento em um software? Repetir código é a mesma coisa que repetir conhecimento?

Vamos considerar todos esse tópicos ao decorrer do artigo.

Alguns motivos que podem levar a duplicidade de conhecimento em um software!

Falta de documentação 🔖

Quando você realmente se aventura no mundo do desenvolvimento de software, descobre que o próprio processo depende de uma documentação detalhada. Sem a documentação desses requisitos, as equipes de desenvolvimento de software podem facilmente se desviar de seus objetivos, ocorrendo em que muitos casos eles podem desenvolver funcionalidades fora dos requisitos ou que são idênticas! Isso pode afetar os prazos de entrega que foram determinados. Ok, mas como a falta de uma documentação técnica e funcional pode trazer a duplicidade de conhecimento no software? Quando você não tem nenhum dos dois para auxiliar no processo de desenvolvimento, as garantias de que aquela funcionalidade já foi solicitada anteriormente e que o requisito agora é de apenas ajustar a feature, ficam bem incertas. Se não conseguimos rastrear por algum meio o que já foi solicitado e comparar com o que já foi desenvolvido, as chances do programador duplicar esse conhecimento no software é consideravelmente alta. O básico é ter uma documentação sólida dos requisitos levantados junto ao cliente e do que foi adicionado ou alterado a cada ciclo de desenvolvimento. Confiar na memória humana pode ser um grave erro! Isso me fez lembrar um projeto que trabalhei, onde cada nova funcionalidade parecia um jogo de adivinhação. Eu e meus colegas nunca conseguimos entender do inicio ao fim todos os requisitos de uma feature. Além disso era novato na empresa e dependia que outros desenvolvedores me explicassem algumas regras gerais da aplicação. Isso custava tempo e também muitos não entendiam completamente tudo o que a aplicação realizava por vários fatores. Muitas vezes tive que por conta própria ler códigos já escritos para entender mais afundo o que o sistema fazia.

Outro ponto que gostaria de destacar é que ao me refirir a uma documentação técnica, não estou falando de documentar linha por linha as funções e descrever o que cada método faz, introduzindo comentários ao código. Testes e o próprio código devem ser a documentação para isso. Mas seria importante documentar os contratos entre front-end e back-end, ou seja, uma documentação apontando o que cada lado espera receber, tanto na request e response. Um exemplo que nos auxilia muito hoje é utilizar o swagger para visualizar melhor esses contratos, assim as equipes podem ter uma documentação para analisar se os contratos batem e se não existem duplicidade de endpoints em suas aplicações.

Prazos curtos ⏳

Todos os projetos de software trabalham com prazos, alguns são realistas outros não. Independente disso existem pressões sobre o tempo que podem levar a decisões precipitadas. Então se precisamos validar algo no sistema, mas simplesmente adicionamos essa validação sem considerar se essa regra já existe, porque não queremos perder tempo perguntando e lendo código; podemos estar adicionando conhecimento duplicado ao software. Se essa validação já ocorre em outra classe e apenas foi implementada de uma forma diferente, ou seja, escrito de forma diferente, estamos duplicando conhecimento e isso não será bom para a qualidade do software! Sobre isso veja o comentário a seguir:

"DRY é sobre a duplicação do conhecimento, da intenção. Trata-se de expressar a mesma coisa em dois lugares diferentes, possivelmente de duas maneiras totalmente diferentes." - O Programador Pragmático: Edição do 20º Aniversário, 2ª Edição.

Se isso se tornar uma rotina e a equipe acabar duplicando conhecimento em sua base de código pois os prazos são curtos, então o software vai ficar volumoso e em breve será difícil de se adicionar novas funcionalidades. Imagine centenas, milhares de linhas que precisam ser alteradas para que o código fique menos volumoso. Além disso as alterações podem levar a introdução de bugs se não forem feitas corretamente e o sistema será bem mais custoso de se realizar modificações e isso claramente afetará prazos determinado pelo cliente! Qual é o ponto aqui? Não abrir mão de planejar e estudar melhor as funcionalidades que devem ser implementadas! Independente se os prazos são curtos ou não, temos que planejar e entender o que será realizado em cada ciclo de desenvolvimento. Quando não planejamos, com certeza vamos duplicar conhecimentos no software.

Falta de comunicação entre equipes 💬

Se em um projeto existem muitas equipes e elas não se comunicam por meio de alguma ferramenta ou até mesmo sinal de fumaça 😅, sempre teremos duplicidade de conhecimento, independente se exista uma documentação bem estruturada e prazos realistas para o desenvolvimento da solução para o cliente.

Muitos podem pensar que as equipes que trabalham no mesmo produto mas em contextos (jornadas) diferentes dificilmente irão ter problemas com duplicidade de conhecimento. Infelizmente não é bem assim, as tarefas podem ser sim em contextos diferentes, mas com certeza em algum momento podem compartilham o mesmo conhecimento sobre algum tipo de regra especifica do sistema. Se não existir uma avaliação sobre essas funcionalidades, ou seja um planejamento, corremos o risco, por exemplo, da equipe A, B e até mesmo a C, em ciclos de desenvolvimento diferentes implementarem conhecimentos que são os mesmos! E isso pode ocorrer tanto no front-end e back-end. Se a equipe A precisa implementar um componente no front-end que é uma modal ou qualquer outro tipo, mas a equipe B também irá precisar realizar isso no futuro e eles não se comunicam, então a duplicidade irá ocorrer. Então podemos ter duas classes que exibem os mesmo tipo de conhecimento, tem validações e ações com os mesmos parâmetros e mensagens idênticas, mas em contextos diferentes. Então nesse caso temos a mesma regra que está espalhada por vários componentes. Isso é duplicar o conhecimento. Não estamos apenas duplicando código. Por isso a comunicação dentro do ciclo de desenvolvimento do software é tão essencial!

Falta de atenção ⚠︎

Talvez a razão mais preocupante pela qual ocorre a duplicação de conhecimento dentro de um sistema. Isso pode ocorrer quando os desenvolvedores ficam isolados em sua própria função e estão preocupados apenas em entregar a tarefa, sem levar em conta a integridade geral do software, zelando pela qualidade. Uma maneira de resolver esse problema é estabelecer uma forte dinâmica de equipe. Por isso é importante estabelecer reuniões de equipe regulares onde todos discutem a visão geral da qualidade do código, garantindo que os desenvolvedores entendam seu papel como parte de um todo e como seu trabalho se conecta com os outros.

Passamos por todos esse fatores cruciais que podem levar a duplicação, existem outros mas no momento acredito que esses sejam os principais e são os que eu já presenciei no ambiente corporativo.

Agora vamos responder a outra pergunta: Qual o risco de se duplicar conhecimento em componentes, módulos ou classes dentro de um software?

Quais os riscos de se duplicar conhecimento?

Dificuldades em realizar manutenções

Se temos uma função, método, classe ou qualquer artefato de software, em algum momento este precisará de manutenção e revisão. Os critérios e requisitos mudam constantemente. David Thomas e Andrew Hunt citam exatamente isso:

"Os programadores estão constantemente em modo de manutenção. Nosso entendimento muda dia a dia. Novos requisitos chegam e os requisitos existentes evoluem à medida que avançamos no projeto. Talvez o ambiente mude. Seja qual for o motivo, a manutenção não é uma atividade discreta, mas uma parte rotineira de todo o processo de desenvolvimento. - O Programador Pragmático: Edição do 20º Aniversário, 2ª Edição."

Por isso pense em um software com duplicação de conhecimento inserida em diferentes classes. Quando um critério ou requisito mudar precisamos realizar manutenção em todos essas classes que contém o mesmo conhecimento espalhado pela base de código. Podemos até citar um exemplo para visualizar como as manutenções podem ficar difíceis quando não aplicamos o DRY:

Um requisito que já foi solicitado e desenvolvido pela equipe é que toda vez que o usuário realizar um upload de um arquivo, precisamos verificar se é um arquivo base64 válido e se o tipo é um PDF. No caso vamos colocar um agravante, existem muitas classes que realizam esse mesmo tipo de verificação e com códigos escritos totalmente diferentes, por desenvolvedores diferentes. Os stakeholders solicitam que antes do usuário enviar o arquivo, é necessário verificar se o PDF está protegido por senha e se estiver devemos impedir que esse arquivo seja carregado, retornando uma mensagem em tela para o usuário final. Certo, então esse é o conhecimento que existe atualmente dentro desse software. Em algum momento o sistema precisa de uma efetiva validação desses arquivos para que o usuário consiga realizar ou continuar com sua jornada dentro da aplicação. Se existem diversas classes que realizam essa validação, com códigos escritos de maneiras diferentes não concorda que isso é duplicar conhecimento? Se possuímos essas validações, com o mesmo objetivo, espalhadas por várias classes imagine a manutenção que será necessária para adicionar um novo requisito. Temos classes diferentes, com códigos escritos de maneiras distintas, mas que cumprem sempre os mesmos objetivos, ou seja, a validação não existe em apenas um lugar, mas está espalhada por muitos componentes. É isso o que o princípio DRY nos ensina a evitar. Na verdade é o que os autores do livro o programador pragmático abordam, veja esse trecho do livro:

"Quando uma única faceta do código precisa mudar, você se vê fazendo essa alteração em vários lugares e em vários formatos diferentes? Você precisa alterar o código e a documentação, ou um esquema de banco de dados e uma estrutura que o contém, ou…? Se sim, seu código não está DRY. - O Programador Pragmático: Edição do 20º Aniversário, 2ª Edição."

Bugs são replicados 🪲

Se temos o costume de simplesmente copiar e colar classes, métodos, apenas renomeando os nomes, isso pode ser muito mais arriscado do que imaginamos! Se aquele trecho de código ou classe, não foi testada adequadamente, podemos estar propagando bugs dentro do software. Quais podem ser os efeitos? Se esses bugs não forem detectados o mais rápido possível, o usuário final provavelmente pode ter dificuldades em utilizar o sistema. Agora pense no esforço que a equipe terá em identificar e corrigir o problema que está propagado por vários componentes do software. Isso pode afetar prazos de entrega! Além disso pode atrasar tarefas de outros desenvolvedores e sobrecarregar todos na equipe. Podemos voltar a ilustração do tópico anterior:

Novas mudanças são exigidas para a validação de upload de arquivos dentro do sistema. Por algum motivo os stakeholders solicitam que a validação aceite arquivos do tipo PNG. Então além de validar se o arquivo base64 é um PDF e se ele está ou não protegido por senha, a funcionalidade deve aceitar também arquivos no formato PNG. Um desenvolvedor que entrou recentemente na equipe é responsável por introduzir esse novo requisito no código. Então ele introduz o novo requisito nas classes mas não realiza os testes unitários de sucesso e para casos de erro. Quando não temos testes não existem garantias de que a função vai realizar exatamente seu papel dentro do sistema. Agora o programador pode ou não ter introduzido um novo bug ao sistema, isso só poderá ser verificado de uma maneira, o usuário final irá de alguma forma enviar um feedback a equipe de desenvolvimento. Depois de um tempo percebe-se que não é possível realizar mais upload de arquivos em algumas rotas que necessitam dessa validação! O desenvolver se pergunta o porque isso está ocorrendo. A resposta é simples; temos a validação espalhada por toda a aplicação, em classes diferentes, mas os códigos foram escritos de maneiras e por desenvolvedores diferentes. Apenas introduzir um novo tipo ou adaptar a lógica de cada componente não é o suficiente. Outro agravante, não temos testes! Agora se inicia tudo novamente, depuração, correção sem testes... é um ciclo praticamente vicioso e que tende a prejuízos financeiros sempre!

Se o conhecimento (regra) estivesse centralizado, escrito de uma maneira clara e com testes, provavelmente essa situação poderia ser evitada!

A produtividade cai

Se a cada novo ciclo de desenvolvimento, as funcionalidades atrasam, são retiradas de produção por causa de bugs e não são atendidas conforme os requisitos que o cliente deseja, pode ter certeza que a produtividade está caindo. Nesses momentos temos que entender o que está por trás da baixa produtividade, se não isso acaba virando uma bola de neve que vai impactar prazos de entrega.

Onde o DRY se encaixa nisso? Em tudo! A produtividade está muito ligada a qualidade do código e planejamento do desenvolvimento das funcionalidades, você aceitando isso ou não! Basta compreender que quanto maior duplicação de conhecimento, mas difícil é entender, revisar e alterar a base de código atual. O que isso leva? Produtividade baixa, não conseguimos produzir nenhum tipo de resultado significativo em prazos determinados pelo cliente.

Agora que analisamos os riscos, vamos responder a outra pergunta.

Como evitar duplicar o conhecimento em um software

Aplicar o DRY em nosso dia a dia é uma questão de analise e observação do progresso do software. Mas podemos nos aprofundar um pouco mais na resposta. Podemos evitar duplicar o conhecimento, por entender melhor os requisitos da funcionalidade e os componentes que já existem no sistema. Além disso podemos evitar por:

  • Levar à sérios as revisões de código e incentivar todos da equipe a fazer isso.
  • Pair programming com desenvolvedores que estão a mais tempo no projeto.
  • Ter documentações dos requisitos independente do tamanho e prazo do projeto.
  • Comunicação entre os times é um fator fundamental para se evitar duplicar o conhecimento dentro do software.
  • Planejar como implementar as funcionalidades de cada ciclo de desenvolvimento.

Agora vamos falar sobre algo muito importante que envolve muito o assunto DRY. Entender a diferença entre duplicar código e conhecimento. Vamos ver isso com alguns exemplos.

Código vs Conhecimento

Podemos citar o próprio livro que traz o princípio Dont Repeat Yourself:

"Nem toda duplicação de código é duplicação de conhecimento... - O Programador Pragmático: Edição do 20º Aniversário, 2ª Edição."

Existe uma clara diferença, é importante entender pois se utilizamos o princípio DRY de uma maneira equivocada, introduzimos complexidade demais ao nosso código sem necessidade. Vamos ao primeiro exemplo:

public class Description {
    // ...
    public bool isLongEnough() {
        String words[] = description.split(' ');
        int numberOfWords = words.length;
        return numberOfWords > 10;
    }
    // ...
}

Aqui a função isLongEnough verifica se a classe Description tem pelo menos 10 palavras. Mas veja o próximo método:

public class ApiResponse {
    // ...
    public bool containsEnoughElements() {
        String elements[] = description.split(' ');
        int numberOfElements = elements.length;
        return numberOfElements > 10;
    }
    // ...
}

Alguns podem pensar que existe uma violação do princípio DRY nestes exemplos, mas a verdade é que não se viola em nenhum momento o princípio. O código é duplicado, mas ambas as funções representam, conhecimentos muito diferentes dentro do software: em um caso, ela representa as regras de validação de uma descrição do usuário, enquanto no outro, ela contém as regras de validação para a resposta de uma API. Veja outro exemplo:

var OS="Unknown";
if (navigator.userAgent.indexOf("Win")!=-1) OS="Windows";
if (navigator.userAgent.indexOf("Mac")!=-1) OS="MacOS";
if (navigator.userAgent.indexOf("X11")!=-1) OS="UNIX";
if (navigator.userAgent.indexOf("Linux")!=-1) OS="Linux";
console.log(OS);
console.log(navigator.userAgent);

O código acima detecta o navegador e sistema operacional do usuário, agora compare com este👇🏼:

var OSName = "unknown";
var navApp = navigator.userAgent.toLowerCase();
switch (true) {
  case (navApp.indexOf("win") != -1):
    OSName = "windows";
    break;
  case (navApp.indexOf("mac") != -1):
    OSName = "apple";
    break;
  case (navApp.indexOf("linux") != -1):
    OSName = "linux";
    break;
  case (navApp.indexOf("x11") != -1):
    OSName = "unix";
    break;
}
console.log(OSName, navApp);

Ignorem a maneira que os códigos foram escritos, servem apenas de exemplo. Os dois códigos cumprem o mesmo objetivo, mas contém estruturas e nomenclaturas diferentes. Isso é um exemplo da violação do princípio DRY se estivessem em várias classes ou componentes do software. Se fosse realmente necessário obter a informação de qual sistema usuário X está logado, poderíamos centralizar em uma função ou classe:

getOperationSystem = () => {
   const systemOperation = ['Windows', 'Linux', 'Mac']; // add your OS values
   return systemOperation.find(system=>navigator.userAgent.indexOf(system) >= 0);
}

console.log(getOperationSystem())

Tudo em excesso pode ser prejudicial

O DRY é extremamente útil, mas devemos ficar atentos ao tentar reutilizar classes e métodos, principalmente quando ainda não entendemos as regras de negócios do software. Não adianta tentar reutilizar todo o código ou prever se aquele método poderá ser reutilizado. O ponto que gostaria de chegar é esse:

"Você não deve aplicar o princípio DRY se sua lógica de negócios ainda não tiver nenhuma duplicação. Novamente, analise o contexto, mas, como regra geral, tentar aplicar DRY a algo que é usado apenas em um lugar pode levar a uma generalização prematura."

A frase acima é bem interessante. Generalizações prematuras podem levar a bugs ou complexidade desnecessária no código! Vamos ver de perto um exemplo mais real, observe as classes abaixo:

/** Envio do armazém para o cliente **/
class Shipment
{
     public deliveryTime = 3; //dias

     public calculateDeliveryDay(): Date
     {
         return new Date(`now ${this.deliveryTime} day`);
     }
}

/** Devolução de pedido de um cliente */
class OrderReturn
{
    public returnLimit = 3; //dias

    public calculateLastReturnDay(): Date
    {
         return new Date(`now ${this.returnLimit} day`);
    }
}

Você diria que estamos repetindo código? Pode ser difícil aceitar mas não estamos, pois o conhecimento aqui não se repete! Como assim? Do ponto de vista do comércio eletrônico, o tempo de entrega de uma remessa para um cliente que é representado pela classe Shipment e o método calculateDeliveryDay(), não tem nada a ver com o último dia em que o cliente pode devolver seus produtos encomendados, que se refere a classe OrderReturn. O que acontece se o desenvolvedor optar por unir os métodos em um só? Se a empresa decidir que o cliente final tem agora vinte dias para devolver seus produtos, você terá que dividir o método novamente. Se você não fizer isso, a entrega da remessa também levará vinte dias! Imagine o impacto isso no mundo real. Os concorrentes ficariam felizes e os clientes provavelmente não! Fica claro que temos regras de negócios diferentes e importantes, que não tem nada haver uma com a outra. O exemplo resume exatamente o que define o DRY, é sobre conhecimento, nunca se tratou de código!

Conclusão

“Don't Repeat Yourself” nunca foi sobre código. É sobre conhecimento. É sobre coesão. Se dois pedaços de código representam exatamente o mesmo conhecimento, eles sempre mudarão juntos. Ter que mudar os dois é muito arriscado: você pode esquecer um deles. Além disso os princípios como o próprio DRY não são regras que você deve seguir sem pensar e analisar com calma. São ferramentas para irmos em uma boa direção. É nosso trabalho adaptá-los dependendo de cada contexto. Certifique-se de ser equilibrado de pesar prós e contras, sempre zelando pela qualidade geral do software, tanto em código e arquitetura.

Espero que tudo o que abordamos tenha sido útil, obrigado por ler até o final!

Referências:

The Pragmatic Programmer: Your Journey to Mastery, 20th Anniversary Edition

The DRY Principle: Benefits and Costs with Examples

Repetition - Artigo escrito por Martin Fowler