DEV Community

Uriel dos Santos Souza
Uriel dos Santos Souza

Posted on • Originally published at programmingisterrible.com

Escreva código que seja fácil de excluir e fácil de depurar também!

Código depuravel é um código que não é mais esperto que você. Alguns códigos são um pouco mais difíceis de depurar do que outros: código com comportamento oculto, tratamento de erros insatisfatório, ambiguidade, muito pouca ou muita estrutura ou código que está no meio de ser alterado. Em um projeto grande o suficiente, você eventualmente encontrará um código que não entende.

Em um projeto antigo o suficiente, você descobrirá o código que esqueceu que escreveu - e se não fosse pelos logs de confirmação, você juraria que era outra pessoa. À medida que um projeto aumenta de tamanho, fica mais difícil lembrar o que cada parte do código faz, mais difícil ainda quando o código não faz o que deveria. Quando se trata de alterar o código que você não entende, você é forçado a aprender da maneira mais difícil: depuração.

Escrever um código fácil de depurar começa com a percepção de que você não se lembrará de nada sobre o código posteriormente.

Regra 0: Um bom código tem falhas óbvias.
Muitos vendedores de metodologias argumentaram que a maneira de escrever um código compreensível é escrever um código limpo. O problema é que “limpo” tem um significado altamente contextual. O código limpo pode ser codificado em um sistema e, às vezes, um hack sujo pode ser escrito de uma maneira fácil de desativar. Às vezes, o código está limpo porque a sujeira foi enviada para outro lugar. Código bom não é necessariamente código limpo.

O código estar limpo ou sujo tem mais a ver com quanto orgulho ou constrangimento o desenvolvedor sente no código, do que com a facilidade de mantê-lo ou alterá-lo. Em vez de limpo, queremos um código enfadonho onde a mudança é óbvia — descobri que é mais fácil fazer com que as pessoas contribuam para uma base de código quando o fruto mais fácil foi deixado para outros coletarem. O melhor código pode ser qualquer coisa que você possa ver e aprender rapidamente.

  • Código que não tenta fazer um problema feio parecer bom, ou um problema chato parecer interessante.
  • Código onde as falhas são óbvias e o comportamento é claro, em vez de código sem falhas óbvias e comportamentos sutis.
  • Código que documenta onde fica aquém da perfeição, em vez de almejar ser perfeito.
  • Código com comportamento tão óbvio que qualquer desenvolvedor pode imaginar inúmeras maneiras diferentes de alterá-lo.

Às vezes, o código é desagradável pra caralho, e qualquer tentativa de limpá-lo deixa você em um estado pior. Escrever um código limpo sem entender as consequências de suas ações também pode ser um ritual de convocação para um código sustentável.

Isso não quer dizer que o código limpo seja ruim, mas às vezes a prática do código limpo é mais semelhante a varrer os problemas para debaixo do tapete. O** código depuravel não é necessariamente limpo**, e o código repleto de verificações ou tratamento de erros raramente torna a leitura agradável.

Regra 1: O computador está sempre pegando fogo.

O computador está pegando fogo e o programa travou na última vez em que foi executado.

A primeira coisa que um programa deve fazer é garantir que está começando de um estado conhecido, bom e seguro antes de tentar realizar qualquer trabalho. Às vezes, não há uma cópia limpa do estado porque o usuário a excluiu ou atualizou o computador. O programa travou na última vez que foi executado e, paradoxalmente, o programa também está sendo executado pela primeira vez.

Por exemplo, ao ler e gravar o estado do programa em um arquivo, vários problemas podem ocorrer:

  • O arquivo está faltando
  • O arquivo está corrompido
  • O arquivo é uma versão mais antiga ou mais recente
  • A última alteração no arquivo está inacabada
  • O sistema de arquivos estava mentindo para você

Estes não são problemas novos e os bancos de dados têm lidado com eles desde o início dos tempos (01/01/1970). Usar algo como o SQLite resolverá muitos desses problemas para você, mas se o programa travou na última vez em que foi executado, o código pode ser executado com os dados errados ou da maneira errada também.

Com programas programados, por exemplo, você pode garantir que os seguintes acidentes ocorrerão:

  • Ele é executado duas vezes na mesma hora por causa do horário de verão.
  • Ele é executado duas vezes porque um operador esqueceu que já havia sido executado.
  • Ele perderá uma hora, devido à falta de disco na máquina ou a problemas misteriosos de rede na nuvem.
  • Levará mais de uma hora para ser executado e pode atrasar chamadas subsequentes do programa.
  • Será executado com a hora errada do dia
  • Ele inevitavelmente será executado próximo a um limite, como meia-noite, final do mês, final do ano e falha devido a erro aritmético.

A criação de um software robusto começa com a criação de um software que assumiu que travou na última vez em que foi executado e trava sempre que não sabe a coisa certa a fazer. A melhor coisa sobre lançar uma exceção em vez de deixar um comentário como “Isso não deveria acontecer” é que, quando inevitavelmente acontecer, você terá uma vantagem inicial na depuração de seu código.

Você também não precisa se recuperar desses problemas - basta deixar o programa desistir e não piorar as coisas. Pequenas verificações que geram uma exceção podem economizar semanas de rastreamento por meio de logs, e um arquivo de bloqueio simples pode economizar horas de restauração do backup.

O código fácil de depurar é o código que verifica se as coisas estão corretas antes de fazer o que foi solicitado, o código que facilita voltar a um bom estado conhecido e tentar novamente e o código que possui camadas de defesa para forçar erros para emergir o mais cedo possível.

Regra 2: Seu programa está em guerra consigo mesmo.

_ Os maiores ataques DoS do Google vêm de nós mesmos - porque temos sistemas realmente grandes - embora de vez em quando alguém apareça e tente nos dar uma corrida pelo nosso dinheiro, mas na verdade somos mais capazes de nos martelar no chão do que qualquer um Mais é._
Isso vale para todos os sistemas.

Astrid Atkinson, livro Engenharia para o Jogo Longo

O software sempre travou na última vez em que foi executado e agora está sempre sem CPU, sem memória e sem disco também. Todas as threads estão martelando uma fila vazia, todos estão tentando novamente uma solicitação com falha que expirou há muito tempo e todos os servidores pausaram para coleta de lixo ao mesmo tempo. Não apenas o sistema está quebrado, mas está constantemente tentando quebrar a si mesmo.

Mesmo verificar se o sistema está realmente em execução pode ser bastante difícil.

Pode ser muito fácil implementar algo que verifique se o servidor está em execução, mas não se está lidando com solicitações. A menos que você verifique o tempo de atividade, é possível que o programa falhe entre cada verificação. As verificações de integridade também podem desencadear bugs: c*onsegui escrever verificações de integridade que travaram o sistema que deveria proteger. Em duas ocasiões distintas*, com três meses de diferença.

Em software, escrever código para lidar com erros inevitavelmente levará à descoberta de mais erros para lidar, muitos deles causados ​​pelo próprio tratamento de erros. Da mesma forma, as otimizações de desempenho muitas vezes podem ser a causa de gargalos no sistema – criar um aplicativo agradável de usar em uma guia pode tornar um aplicativo difícil de usar quando você tem vinte cópias dele em execução.

Outro exemplo é quando um trabalhador(thread) em um pipeline está executando muito rápido e esgotando a memória disponível antes que a próxima parte tenha a chance de alcançá-lo. Se preferir uma metáfora de carro: engarrafamentos. Acelerar é o que os cria e pode ser visto na maneira como o congestionamento se move para trás no trânsito. As otimizações podem criar sistemas que falham sob carga alta ou pesada, geralmente de maneiras misteriosas.

Em outras palavras: quanto mais rápido você fizer isso, mais difícil será empurrado e, se você não permitir que seu sistema empurre para trás nem um pouco, não se surpreenda se ele quebrar.

A contrapressão é uma forma de feedback dentro de um sistema, e um programa fácil de depurar é aquele em que o usuário está envolvido no loop de feedback, tendo uma visão de todos os comportamentos de um sistema, o acidental, o intencional, o desejado, e os indesejados também. O código depuravel é fácil de inspecionar, onde você pode observar e entender as mudanças que estão acontecendo.

Regra 3: O que você não elimina a ambiguidade agora, você depura mais tarde.
Em outras palavras: não deve ser difícil olhar para as variáveis ​​em seu programa e descobrir o que está acontecendo. Com ou sem algumas sub-rotinas de álgebra linear aterrorizantes, você deve se esforçar para representar o estado do seu programa da forma mais óbvia possível. Isso significa coisas como não mudar de ideia sobre o que uma variável faz no meio de um programa, se houver um pecado capital óbvio, é usar uma única variável para dois propósitos diferentes.

Também significa evitar cuidadosamente o problema do semipredicado, nunca usando um único valor ( count) para representar um par de valores ( boolean, count). Evitando coisas como retornar um número positivo para um resultado e retornar -1 quando nada corresponder. O motivo é que é fácil acabar na situação em que você deseja algo como "0, but true"(e notavelmente, o Perl 5 tem esse recurso exato) ou cria um código difícil de compor com outras partes do sistema ( -1 pode ser uma entrada válida para a próxima parte do programa, em vez de um erro).

Juntamente com o** uso de uma única variável para dois propósitos, pode ser tão ruim quanto usar um par de variáveis ​​para um único propósito** — especialmente se forem booleanas. Não quero dizer que manter um par de números para armazenar um intervalo seja ruim, mas usar vários booleanos para indicar em que estado seu programa está geralmente é uma máquina de estado disfarçada.

Quando o estado não flui de cima para baixo, dê ou tire o loop ocasional, é melhor dar ao estado uma variável própria e limpar a lógica. Se você tiver um conjunto de booleanos dentro de um objeto, substitua-o por uma variável chamada estado e use um enum (ou uma string se persistir em algum lugar). As instruções if acabam parecendo if state == name e param de parecer if bad_name && !alternate_option.

Mesmo quando você torna a máquina de estado explícita, ainda pode errar: às vezes o código tem duas máquinas de estado escondidas dentro. Tive grande dificuldade em escrever um proxy HTTP até tornar cada máquina de estado explícita, rastreando o estado da conexão e analisando o estado separadamente. Quando você mescla duas máquinas de estado em uma, pode ser difícil adicionar novos estados ou saber exatamente em que estado algo deve estar.

Isso é muito mais sobre criar coisas que você não terá que depurar, do que tornar as coisas fáceis de depurar. Ao elaborar a lista de estados válidos, é muito mais fácil rejeitar os inválidos completamente, em vez de deixar acidentalmente um ou dois passarem.

Regra 4: Comportamento acidental é um comportamento esperado.

Quando você não tem certeza sobre o que uma estrutura de dados faz, os usuários preenchem as lacunas — qualquer comportamento do seu código, intencional ou acidental, acabará por ser considerado em outro lugar. Muitas linguagens de programação convencionais tinham tabelas de hash pelas quais você podia iterar, o que meio que preservava a ordem de inserção, na maioria das vezes.

Algumas linguagens optaram por fazer a tabela hash se comportar como muitos usuários esperavam, iterando pelas chaves na ordem em que foram adicionadas, mas outras optaram por fazer a tabela hash retornar as chaves em uma ordem diferente, a cada iteração. No último caso, alguns usuários reclamaram que o comportamento não era aleatório o suficiente.

Tragicamente, qualquer fonte de aleatoriedade em seu programa será eventualmente usada para fins de simulação estatística, ou pior, criptografia, e qualquer fonte de ordenação será usada para classificação.

Em um banco de dados, alguns identificadores carregam um pouco mais de informação do que outros. Ao criar uma tabela, um desenvolvedor pode escolher entre diferentes tipos de chave primária. A resposta correta é um UUID ou algo indistinguível de um UUID. O problema com as outras opções é que elas podem expor informações de pedido, bem como identidade, ou seja, não apenas se, a == b mas se a <= b, e outras opções significam chaves de incremento automático.

Com uma chave de incremento automático, o banco de dados atribui um número a cada linha da tabela, adicionando 1 quando uma nova linha é inserida. Isso cria uma espécie de ambiguidade: as pessoas não sabem qual parte dos dados é canônica. Em outras palavras: você classifica por chave ou por carimbo de data/hora? Como nas tabelas de hash anteriores, as pessoas decidirão a resposta certa por si mesmas. O outro problema é que os usuários também podem adivinhar facilmente os outros registros de chaves próximos.

Em última análise, qualquer tentativa de ser mais inteligente do que um UUID sairá pela culatra: já tentamos com códigos postais, números de telefone e endereços IP, e falhamos miseravelmente todas as vezes. Os UUIDs podem não tornar seu código mais depuravel, mas um comportamento menos acidental tende a significar menos acidentes.

A ordem não é a única informação que as pessoas extrairão de uma chave: se você criar chaves de banco de dados construídas a partir de outros campos, as pessoas jogarão fora os dados e os reconstruirão a partir da chave. Agora você tem dois problemas: quando o estado de um programa é mantido em mais de um lugar, é muito fácil que as cópias comecem a discordar umas das outras. É ainda mais difícil mantê-los sincronizados se você não tiver certeza de qual deles precisa alterar ou qual deles você alterou.

Tudo o que você permitir que seus usuários façam, eles implementarão. Escrever código depuravel é pensar antecipadamente sobre as maneiras pelas quais ele pode ser mal utilizado e como outras pessoas podem interagir com ele em geral.

Regra 5: A depuração é social, antes de ser técnica.

Quando um projeto de software é dividido em vários componentes e sistemas, pode ser consideravelmente mais difícil encontrar bugs. Depois de entender como o problema ocorre, talvez seja necessário coordenar as alterações em várias partes para corrigir o comportamento. Corrigir bugs em um projeto maior é menos sobre encontrar os bugs e mais sobre convencer as outras pessoas de que eles são reais, ou mesmo que uma correção é possível.

Os bugs persistem no software porque ninguém tem certeza absoluta de quem é o responsável pelas coisas. Em outras palavras, é mais difícil depurar o código quando nada está escrito, tudo deve ser perguntado no Slack e nada é respondido até que a única pessoa que sabe faça logon.

Planejamento, ferramentas, processo e documentação são as maneiras pelas quais podemos corrigir isso.

O planejamento é como podemos remover o estresse de estar de plantão, com estruturas para gerenciar incidentes. Os planos são como mantemos os clientes informados, trocamos pessoas quando estão de plantão por muito tempo e como rastreamos os problemas e introduzimos mudanças para reduzir riscos futuros. As ferramentas são a maneira pela qual desqualificamos o trabalho e o tornamos acessível a outras pessoas. O processo é a maneira pela qual podemos remover o controle do indivíduo e entregá-lo à equipe.

As pessoas vão mudar, as interações também, mas os processos e ferramentas serão mantidos à medida que a equipe mudar com o tempo. Não é tanto valorizar um mais do que o outro, mas construir um para apoiar mudanças no outro. O processo também pode ser usado para remover o controle da equipe, então nem sempre é bom ou ruim, mas sempre há algum processo de trabalho, mesmo quando não está escrito, e o ato de documentá-lo é o primeiro passo para permitir que outras pessoas o modifiquem.

A documentação significa mais do que arquivos de texto: a documentação é como você transfere responsabilidades, como atualiza as novas pessoas e como comunica o que mudou para as pessoas afetadas por essas mudanças. Escrever documentação requer mais empatia do que escrever código, e mais habilidade também: não há flags de compilador ou verificadores de tipo fáceis, e é fácil escrever muitas palavras sem documentar nada.

Sem documentação, como você pode esperar que as pessoas tomem decisões informadas ou até mesmo concordem com as consequências do uso do software? Sem documentação, ferramentas ou processos, você não pode dividir o ônus da manutenção ou mesmo substituir as pessoas atualmente sobrecarregadas com a tarefa.

Tornar as coisas fáceis de depurar se aplica tanto aos processos em torno do código quanto ao próprio código, deixando claro em quem você terá que se apoiar para consertar o código.

Código fácil de depurar é fácil de explicar.
Uma ocorrência comum durante a depuração é perceber o problema ao explicá-lo para outra pessoa. A outra pessoa nem precisa existir, mas você tem que se forçar a começar do zero, explicar a situação, o problema, os passos para reproduzi-lo, e muitas vezes esse enquadramento é suficiente para nos dar uma ideia da resposta.

Se apenas. Às vezes, quando pedimos ajuda, não pedimos a ajuda certa, e sou tão culpado disso quanto qualquer um - é uma aflição tão comum que tem nome: “O problema XY”: como faço para obter o últimas três letras de um nome de arquivo? Oh? Não, eu quis dizer a extensão do arquivo.

Falamos sobre problemas em termos das soluções que entendemos e falamos sobre as soluções em termos das consequências que conhecemos. Depurar é aprender da maneira mais difícil sobre consequências inesperadas e soluções alternativas, e envolve uma das coisas mais difíceis que um programador pode fazer: admitir que fez algo errado.

Afinal, não era um bug do compilador.

Fonte: programming is terrible lessons learned from a life wasted
https://programmingisterrible.com/post/173883533613/code-to-debug

Top comments (0)