Um humano poderia entender isso?
Cada decisão que tomamos enquanto programamos tem potencial para causar uma série de resultados - alguns desejáveis, outros não!
Martin Fowler já dizia:
Qualquer idiota pode escrever código que uma máquina pode entender. Bons programadores escrevem código que humanos podem entender.
Esta frase, extraída de seu influente livro Refactoring: Improving the Design of Existing Code, é muito significativa para todos os desenvolvedores que se importam não apenas com a funcionalidade do seu software, mas também com a sua manutenibilidade, testabilidade, legibilidade e sua eficácia.
Enquanto isso, o lendário Kent Beck, um dos fundadores da programação extrema e um defensor da simplicidade no design de software, nos traz uma perspectiva semelhante:
A maior limitação do software é a habilidade do desenvolvedor de entender o sistema.
Esta frase, originária de seu livro "Extreme Programming Explained", revela a verdade inevitável de que o software não é limitado apenas pelas restrições técnicas, mas também pela compreensão humana.
Juntas, essas duas frases ressaltam um princípio que deveria ser a pedra angular de qualquer projeto de software: a clareza e simplicidade do código são vitais. Não se trata apenas de uma questão estética ou de vaidade intelectual. Trata-se de uma preocupação pragmática que afeta a eficiência e a eficácia de toda a equipe de desenvolvimento.
Mas, antes de continuarmos, deixe-me esclarecer uma coisa. Quando me refiro a clareza e simplicidade, não estou sugerindo que sacrifique a funcionalidade ou o desempenho em prol de um design de codificação mais "bonito". Não, não é isso. Estamos falando de escrever um código que seja facilmente compreendido por seus colegas programadores, que possa ser mantido, estendido e aperfeiçoado sem a necessidade de uma garrafa gigante de analgésicos.
Então, o que significa escrever um código que humanos possam compreender? E por que isso é tão importante? Vamos mergulhar nessas questões e descobrir.
Código Inteligível: Por Que Importa?
O software é compreendido, modificado e estendido várias vezes ao longo de sua vida útil. Isso torna a legibilidade do código uma característica indispensável em qualquer projeto. Mas o que isso realmente significa?
Na prática, um código compreensível é aquele que não precisa de um decodificador ou um intérprete para ser entendido. O código não deve ser um enigma, não deve ser um labirinto, e definitivamente não deve ser uma caixa preta. Deve ser um livro aberto, pronto para ser lido, compreendido e expandido por qualquer membro da equipe.
Como Kent Beck destaca, a habilidade do programador em entender o sistema é a maior limitação do software. Portanto, se a equipe de desenvolvimento não consegue entender um sistema claramente, como eles poderão modificá-lo, corrigi-lo ou melhorá-lo? Como eles poderão garantir que não estão introduzindo novos bugs cada vez que fazem uma alteração? Como será possível fazer entregas pontuais? É por isso que precisamos lutar e nos esforçar para escrever algoritmos legíveis independente do nível de senioridade. Não importa se você tem 5, 10, 15 ou 20 anos de empresa, todos precisam zelar pela qualidade e clareza do código!
Além disso, isso importa porque um algoritmo legível também reduz o tempo e o esforço necessários para trazer novos membros da equipe a par do que está sendo feito em determinado fluxo no software. Quanto mais fácil for para um novo desenvolvedor entender o que uma classe ou método faz, mais rápido ele poderá contribuir de forma efetiva para o projeto.
Além disso, um código simples e de fácil compreensão pode ajudar a reduzir a incidência de erros. E o fato é que os humanos são muito melhores em detectar erros em códigos que eles possam ler e entender claramente.
Então, quais são os benefícios? A lista é extensa, mas podemos destacar algumas vantagens principais:
Eficiência no desenvolvimento: Um código limpo e compreensível torna o processo de desenvolvimento mais eficiente. Ele permite que a equipe se concentre em adicionar novas funcionalidades, em vez de gastar tempo decifrando um código obscuro e complexo.
Facilidade de manutenção: A manutenção do software é uma parte inevitável de seu ciclo de vida. Um código de fácil compreensão e simples torna a manutenção uma tarefa muito mais fácil e menos propensa a erros.
Código mais fácil de revisar: Quando escrevemos um método ou classe que é simples ela será mais fácil de revisar, o que aumenta as chances de encontrar possíveis problemas de lógica.
Melhoria na colaboração da equipe: Um código fácil de ler e compreender promove a colaboração dentro da equipe. Permite que todos os membros da equipe contribuam para o projeto de forma efetiva, independentemente de seu nível de experiência.
O ponto é, software é menos sobre máquinas e mais sobre pessoas. As máquinas apenas executam os comandos que são escritos, fornecidos pelos seres humanos. São as pessoas que criam, mantêm e evoluem o software. Portanto, um código que é fácil para as pessoas entenderem tem um grande valor intrínseco. É como uma estrada bem pavimentada: permite que você chegue ao seu destino mais rápido, mais fácil e de uma maneira mais segura.
Desenvolvimento de software não é apenas um exercício de comunicação com a máquina, mas ainda mais, uma questão de comunicação entre humanos.
Mas o que significa exatamente um design"limpo" e "simples"? Uma maneira de pensar sobre isso é através da lente do Princípio KISS (Keep It Simple, Stupid). O princípio é direto, evite a complexidade desnecessária, crie a coisa mais simples possível que atenda as necessidades do cliente. E para isso ser alcançado cada método deve ter responsabilidades bem definidas. As classes devem ser pequenas e coesas. O código deve ser autoexplicativo tanto quanto possível.
No entanto, apesar da evidente lógica por trás disso, é incrivelmente fácil cair na armadilha de escrever um código excessivamente complexo. Talvez seja por causa da pressão para entregar rapidamente, talvez seja por causa da tentação de mostrar quão inteligente somos, ou talvez seja apenas por falta de disciplina e experiência.
Seja qual for o motivo, o resultado é um código que parece mais um labirinto do que uma estrada. Ele está cheio de loops, voltas desnecessárias e reviravoltas que dificultam a navegação. E assim, o que deveria ser uma tarefa direta de leitura e compreensão do código se transforma em um desafio que consome tempo e energia.
Precisamos evitar isso! Por que? Porque precisamos facilitar o trabalho dos nossos colegas de equipe, revisores, ou até mesmo nós mesmos, meses depois, quando esquecemos o que nossa própria lógica tortuosa estava tentando realizar. Portanto, um bom código deve ser transparente em sua intenção, lógico em seu fluxo e, acima de tudo, legível para uma pessoa com conhecimentos equivalentes na linguagem de programação.
A frase de Kent Beck citada no inicio do artigo nos lembra que, por mais inteligentes e capazes que sejamos, nossas habilidades cognitivas são limitadas. Não podemos manter uma representação precisa de um sistema incrivelmente complexo em nossas cabeças, e quando tentamos, inevitavelmente perdemos o controle dos detalhes. Isso resulta em bugs, regressões, e na famosa expressão "funciona, mas não sei por quê".
Então, o que podemos fazer para lutar contra a complexidade e garantir que nossos sistemas sejam compreensíveis para os humanos?
Corra atrás da simplicidade!
O primeiro incentivo é abraçar a simplicidade. Isso pode parecer um conselho trivial, mas na prática pode ser incrivelmente desafiador. A simplicidade muitas vezes exige que sejamos disciplinados para resistir à tentação de adicionar "apenas mais um recurso", ou utilizar aquele novo design pattern brilhante que acabamos de aprender, quando uma solução mais simples e direta poderia servir tão bem ou melhor.
A simplicidade também implica em transformar nosso código o mais declarativo possível. Em vez de se concentrar em como algo deve ser feito (o que muitas vezes leva a algoritmos imperativos enigmáticos), devemos nos esforçar para descrever o que queremos que seja feito, deixando os detalhes da implementação para as bibliotecas de software e a linguagem de programação que estamos usando. Isso torna nosso código mais fácil de ler e entender, e também abre a porta para otimizações que a máquina pode realizar melhor do que nós.
É verdade que abraçar a simplicidade é uma tarefa desafiadora. É um exercício de contenção e disciplina. Resistir à tentação de adicionar "apenas mais um recurso", evitar o uso de design patterns complexos quando uma solução mais simples serve igualmente bem, exige um grau de autocontrole que muitas vezes falta em nossa pressa de produzir e entregar. E isso muitas vezes é um grande obstáculo!
Por que é tão difícil manter as coisas simples?
Uma razão pode ser que, como desenvolvedores, tendemos a nos apaixonar por nossos próprios algoritmos. Tratamos cada linha como um artista trata cada pincelada, cada nota como um compositor trata sua sinfonia. O problema é que, ao contrário da arte ou da música, o código não é para ser apreciado por sua beleza intrínseca. Código é uma ferramenta para resolver problemas.
Mas sabemos que os engenheiros de software enfrentam uma série de decisões de design. Algumas dessas escolhas podem levar o software para fora do caminho da simplicidade. Podemos comentar os principais:
Otimização prematura: É comum que desenvolvedores caiam na tentação de otimizar prematuramente, tentando atingir um desempenho incrível antes mesmo de terem certeza de que o código está funcionando corretamente. Isso pode levar a estruturas complexas, difíceis de entender e manter. Como Donald Knuth disse: "A otimização prematura é a raiz de todo mal".
Arquiteturas Over-Engineered: Muitas vezes, projetamos arquiteturas complexas com muitos componentes interconectados, antecipando-se a possíveis requisitos futuros. Embora seja importante considerar o crescimento e a evolução do software, a criação de arquiteturas over-engineered pode adicionar uma complexidade desnecessária e tornar o código mais difícil de entender e manter.
Ignorar o débito técnico: O tão comentado débito técnico refere-se a decisões de desenvolvimento que podem facilitar o progresso a curto prazo, mas que provavelmente causarão problemas a longo prazo. Ignora-lo pode resultar em um código mais complexo e difícil de manter ao longo do tempo.
Todos esses exemplos servem como lembretes de que a simplicidade no design do software é um objetivo que requer um equilíbrio cuidadoso. Manter a simplicidade em mente ao tomar decisões de design pode ajudar a evitar essas armadilhas e criar um design mais legível, fácil de manter e testar .
Alcançar a simplicidade pode parecer uma tarefa desafiadora, mas pode ser melhorar direcionada ao seguir alguns passos:
Entender o Problema: Antes de escrever uma única linha de código, é fundamental compreender o problema que você está tentando resolver. Quanto mais claro o entendimento do problema, mais fácil será identificar a solução mais simples e direta. Vamos falar mais sobre isso em breve.
Planejar Antes de Programar: Pense na solução antes de começar a programar. Esboce o fluxo do programa e os principais componentes. Isto pode ajudá-lo a identificar a abordagem mais simples para implementar a solução.
Princípio KISS (Keep It Simple, Stupid): Este princípio é um lembrete constante para manter o seu código tão simples quanto possível. Sempre que você adicionar complexidade desnecessária, dê um passo atrás e pense em como pode simplificar.
Usar a Programação Declarativa Quando Possível: Como mencionado anteriormente, a programação declarativa, que descreve o que o programa deve realizar sem explicitar como, pode levar a um código mais simples e mais fácil de entender.
Refatoração Regular: A refatoração é o processo de alterar a estrutura do código sem alterar o seu comportamento para melhorar a legibilidade e reduzir a complexidade. Fazer isso regularmente pode ajudar a manter a simplicidade do seu código.
Escrever Testes Unitários: Testes unitários não só ajudam a garantir que o que foi escrito funciona como esperado, mas também forçam você a escrever código que é testável. Quando pensamos na testabilidade tendemos a escrever de maneira mais modular e mais simples.
Revisão de Código e Feedback: A revisão de código por pares e o feedback são ótimas maneiras de garantir que cada linha seja compreensível e simples. Se outros desenvolvedores entenderem o que está escrito, é provável que você esteja no caminho certo.
Vamos ver um exemplo claro que pode ajudar a explicar melhor do que se trata as decisões.
Quanto mais simples de ler, melhor!
Quando pensamos em escrever ou melhorar um determinado trecho de código, devemos primeiro simplificar a leitura. Provavelmente já enfrentou esse problema. Vamos utilizar como exemplo o uso excessivo de operadores lógicos em uma única estrutura condicional. É natural utilizar esses recursos da linguagem, pois são fundamentais para o controle do fluxo do software. No entanto, condições excessivamente complexas podem tornar o software difícil de entender e manter. Olhe para o exemplo abaixo:
Há muitas verificações acontecendo em uma única linha. Uma pessoa que está lendo pela primeira vez pode ter dificuldade para compreender a lógica que está sendo aplicada. As condições para aprovar o empréstimo estão todas misturadas, e é preciso um certo esforço cognitivo para discernir o que exatamente está sendo verificado. Isso pode tornar o código mais suscetível a erros, pois é mais difícil de ler e, portanto, mais difícil de revisar e depurar.
É importante destacar a falta de modularidade, isso é preocupante e mostra que algo no design e nas escolhas está exalando alguns cheiros ruins. A lógica da aprovação do empréstimo está sendo realizada praticamente em um único método. Isso dificulta a manutenção e a adição de novas validações ou modificações nas que já existem. Fica dificil escrever também os testes unitários, podemos acabar perdendo algum cenário de teste importante!
Vamos imaginar que no futuro você precise mudar a lógica de avaliação de crédito, talvez seja necessário um mínimo de 750 ao invés de 700. Agora você teria que encontrar essa linha específica de código e alterá-la, arriscando, no processo, a introdução de um bug.
Por último, mas não menos importante, há uma clara falta de transparência sobre as regras de negócio que esse código está tentando implementar. O programador que ler terá que interpretar a lógica por trás das condições para entender o que é considerado um cliente elegível para o empréstimo. Isso pode ser um grande problema, especialmente quando novos desenvolvedores se juntam à equipe ou quando é necessário revisar as regras de negócio.
Esses três fatos destacam a importância de manter a simplicidade e a clareza ao escrever código.
Essa refatoração deixa as coisas mais claras. Pode ser que até se pergunte: “Por que no método IsCustomerEligibleForLoan()
, o retorno não foi passado direto?” Como na imagem abaixo:
Bom acho importante explicar isso. Definir as condições da elegibilidade como variáveis booleanas separadas, tal como isAgeEligible
, hasGoodCreditScore
e isEmployed
, traz vantagens e desvantagens que dependem principalmente do contexto em que estão inseridas.
Uma das vantagens dessa abordagem é a facilidade para depuração. Ao definir condições complexas como variáveis separadas, é mais fácil rastrear qual condição específica falhou durante a execução do código. Por exemplo, se o método IsCustomerEligibleForLoan
retornar false
, seria fácil verificar qual das condições retornou false
através da depuração ou até mesmo do log, dependendo de como o código foi escrito.
Além disso, essa abordagem também pode melhora a leitura e manutenção. Ao atribuir condições a variáveis bem nomeadas, estamos essencialmente documentando o código. Um futuro desenvolvedor (ou até mesmo você no futuro) terá uma compreensão mais imediata do que cada condição está verificando, sem precisar decifrar uma expressão booleana.
Mas nem tudo é festa, essa abordagem também tem suas desvantagens. Primeiro, pode levar a um código mais verboso. Em casos onde as condições são bastante simples e a legibilidade não é seriamente comprometida, pode ser mais eficiente combinar todas as condições em uma única expressão de retorno.
Segundo, pode ocorrer uma leve perda de eficiência, pois todas as condições são avaliadas, mesmo que uma condição precoce falhe. Isso pode não ser perceptível na maioria dos casos, mas em um ambiente de alto desempenho, cada pequena otimização conta. Leve esses pontos em consideração para seu contexto!
Um ponto muito essencial que gostaria de destacar é sobre evitar números mágicos em nosso código, vamos direto aos beneficios de se evita-los:
Clareza de significado: Quando você vê um número como
700
ou60
, pode ser difícil entender o que esses números significam. Ao utilizar uma constante com um nome descritivo, comoMINIMUM_CREDIT_SCORE
ouMAXIMUM_AGE
, fica imediatamente claro para o leitor do código qual é o significado desses valores.Facilidade de manutenção: Suponha que você precise mudar o valor do
MINIMUM_CREDIT_SCORE
de700
para750
. Se esse valor for usado em vários lugares e você tiver usado o número700
diretamente, você teria que procurar todas as instâncias do número700
e mudar cada uma delas para750
. Isso pode ser propenso a erros, pois nem todos os700
em seu código podem se referir aoMINIMUM_CREDIT_SCORE
. No entanto, se você usou uma constante, você só precisa mudar o valor da constante em um lugar.Redução de erros: Quando você usa números diretamente no código, há uma maior chance de cometer erros, como digitar
800
em vez de80
. Com constantes, esses erros são menos prováveis, porque é definido o valor da constante apenas uma vez.Consistência e reutilização: Se você tem um valor que é usado em vários lugares, definir esse valor como uma constante garante que o mesmo valor seja usado em todos os lugares. Isso é especialmente útil se o valor for algo complicado ou se houver uma chance de o valor mudar no futuro.
O exemplo que foi apresentado claramente tem suas limitações para alguns contextos, mas a essencia da mensagem permanece a mesma, escreva código para humanos, para facilitar o trabalho de todos!
Questione, questione e questione!
A primeira reflexão que devemos no fazer após finalizar a escrita de qualquer linha é: Um humano poderia compreender isso? Depois podemos fazer outras seguindo essa linha de raciocínio:
O que foi escrito é estruturado de forma que um humano possa facilmente escrever testes?
Os nomes das variáveis e funções permitem que um humano entenda facilmente o que cada componente está fazendo?
O código está estruturado de forma a minimizar a quantidade de tempo que um humano passaria lendo e compreendendo?
Existe algum trecho de código que parece ininteligível e que poderia ser reescrito para melhorar a facilidade de leitura por um humano?
O código está escrito de maneira que um humano pode facilmente prever o resultado de qualquer modificação feita no código?
Mas é importante não se perder! Foque em criar código que cumpra o objetivo esperado, mas ao mesmo tempo avalie o quão claro é compreender o fluxo e cada regra de negócio que foi escrita.
Para obter a simplicidade entenda o problema!
Esse é um grande alerta! Quanto mais entendemos sobre o problema que estamos tentando resolver, mais fácil será pensar e escrever códigos que humanos possam entender. Por que isso é verdade?
O programador é, antes de tudo, um solucionador de problemas. Portanto, a tarefa fundamental que todos nós temos é compreender o problema antes de se lançar à sua resolução. Este entendimento profundo é, em si, uma poderosa ferramenta para criar código legível, eficiente e fácil de manter.
Para compreendermos melhor esta afirmação, devemos considerar o processo de codificação não apenas como uma tarefa técnica, mas também como uma atividade intelectual e criativa. Escrever código é, em essência, traduzir um problema do mundo real para uma linguagem que a máquina possa compreender e, assim, produzir a solução desejada.
Vamos visualizar isso melhor. Quando um programador não entende claramente o problema, as ramificações podem ser bastante negativas.
As ramificações são as possíveis consequências e desdobramentos que surgem a partir de uma escolha particular. Cada decisão que tomamos enquanto programamos tem potencial para causar uma série de resultados - alguns desejáveis, outros não. Esses desdobramentos, ou ramificações, podem afetar o curso do projeto a curto e a longo prazo.
Entender as ramificações é crucial para tomar decisões informadas. Quando você entende o que pode acontecer com base em uma escolha específica, você está mais equipado para tomar uma decisão que será benéfica para o projeto a longo prazo.
Por exemplo, se decidimos implementar uma funcionalidade de forma rápida e não estruturada, ou seja, não paramos para analisar o problema e não planejamos, mas fazemos apenas para atender a uma demanda imediata, a ramificação imediata pode ser a satisfação da necessidade urgente. Entretanto, as ramificações a longo prazo dessa decisão podem incluir um código de difícil manutenção, com mais chances de erros e mais tempo gasto em futuras alterações dessa funcionalidade. Como no desenho abaixo tenta ilustrar isso:
Essas decisões, e suas consequentes ramificações, podem ser visualizadas como uma árvore de decisão, onde cada nó representa uma escolha e cada ramo representa um possível resultado. Nesse sentido, é possível fazer uma comparação com a teoria dos grafos na matemática, onde o objetivo é encontrar o caminho mais eficiente entre dois pontos. A cada decisão tomada, o número de possíveis caminhos futuros (ou ramificações) aumenta, e cada um desses caminhos tem suas próprias implicações e possíveis resultados.
Portanto, é crucial que os programadores tenham uma compreensão clara não apenas do problema imediato que estão tentando resolver, mas também de como suas decisões podem impactar o futuro do projeto.
Quando compreendemos profundamente o problema, estamos aptos a visualizar a melhor estratégia para solucioná-lo. Esse entendimento permite que o cada um de nós identifique quais partes do problema são cruciais, quais são secundárias e quais podem ser decompostas em subproblemas menores. Com esse panorama em mente, podemos estruturar de maneira lógica e coerente cada linha, facilitando a leitura e compreensão por outros desenvolvedores.
Outro ponto que já me deparei nos últimos anos é que um profundo entendimento do problema permite a escrita de um código que realmente atende às necessidades do usuário. Muitas vezes, na pressa de produzir uma solução, podemos acabar escrevendo algo que, embora correto, não atende de maneira eficaz ao problema que o cliente possui. Compreender o problema em profundidade permite evitar esse tipo de descompasso.
Conclusão
Para finalizar, a questão "Um humano poderia entender isso?" é fundamental no desenvolvimento de software. Ela incide diretamente sobre a legibilidade do código, a manutenibilidade do sistema e, em última análise, a qualidade do software produzido. A compreensibilidade humana deve estar na vanguarda das considerações do programador, não apenas para o benefício imediato dos colegas de equipe ou para si mesmo no futuro, mas também para a sustentabilidade do projeto a longo prazo.
Se um código é escrito de uma maneira que um humano pode entender facilmente, é provável que seja mais eficiente para testar, depurar, modificar e estender. Além disso, essa abordagem promove práticas de codificação melhores e mais limpas, o que leva a sistemas mais robustos e confiáveis.
A simplicidade é, portanto, uma escolha deliberada que os programadores devem fazer para alcançar algoritmos claros e legíveis. Assim como um autor de um livro se esforça para garantir que sua mensagem seja compreendida pelo leitor, um programador deve se esforçar para garantir que seu código seja facilmente compreendido por outros programadores. Esta é a parte que muitos programadores acabam deixando escapar e a ciência do desenvolvimento de software, uma intersecção de tecnologia e humanidade onde cada linha de código é uma decisão tomada, cada decisão tem ramificações e cada ramificação tem um impacto no entendimento humano.
Assim, quando se deparar com a pergunta, "Um humano poderia entender isso?", um programador deve ter a consciência de que a resposta a essa pergunta pode ser o divisor entre um software sustentável e eficiente e um software problemático e complexo. Em última análise, a meta é criar códigos que os humanos não apenas possam entender, mas que sejam convidativos, fáceis de trabalhar e de manter.
Fico por aqui neste post. Se gostou, por favor compartilhe com outros programadores! Se desejar comente. Estou à disposição para tirar dúvidas também pelo LinkedIn. Até o próximo post pessoal! 😉