
Fácil de depurar código é um código que não engana você. É mais difícil depurar código com comportamento oculto, com tratamento inadequado de erros, com incertezas, insuficientemente ou excessivamente estruturadas ou no processo de alteração. Em projetos grandes o suficiente, você acaba com um código que não consegue entender.
Se o projeto for relativamente antigo, você poderá encontrar o código que esqueceu, e se não fosse o log de confirmação, juraria que essas linhas não foram escritas por você. À medida que o projeto cresce, fica mais difícil lembrar o que diferentes partes do código fazem. E a situação é agravada se o código não fizer o que parece estar fazendo. E quando você precisa alterar um código que não entende, é preciso entender bem: depurar.
A capacidade de escrever um código fácil de depurar começa com o entendimento de que você não se lembra de nada escrito anteriormente.
Regra 0: Bom código contém erros óbvios.
Fornecedores de tecnologia amplamente usada afirmam que "escrever código claro" significa "escrever código limpo". O problema é que o grau de "pureza" é altamente sensível ao contexto. O código puro pode ser codificado no sistema e, às vezes, algum truque sujo é gravado para facilitar a desativação. Às vezes, o código é considerado limpo, porque toda a sujeira é jogada em algum lugar. Um bom código não é necessariamente limpo.
A limpeza caracteriza o grau de orgulho (ou vergonha) experimentado pelo desenvolvedor em relação a este código, em vez da facilidade de manutenção ou alteração. É melhor nos fornecer um código chato, em vez de um código limpo, cujas mudanças são óbvias: descobri que as pessoas estão mais dispostas a modificar a base de código se a fruta ficar baixa o suficiente e for fácil de escolher. O melhor código pode ser o que você acabou de ver e imediatamente entendeu como funciona.
- Código que não tenta criar um problema feio para parecer bom ou um problema chato para parecer interessante.
- Código no qual os erros são óbvios e o comportamento é claro, diferente do código sem erros óbvios e com comportamento pouco claro.
- O código no qual está documentado, no qual não é ideal, em contraste com o código que busca a perfeição.
- Codifique com um comportamento tão óbvio que qualquer desenvolvedor pode criar uma infinidade de maneiras diferentes de modificar esse código.
Às vezes, o código é tão desagradável que qualquer tentativa de torná-lo mais limpo apenas exacerba a situação. Escrever código sem entender as conseqüências de suas ações também pode ser considerado um ritual de invocação de código mantido convenientemente.
Não quero dizer que o código limpo é ruim, mas às vezes o desejo de limpeza parece mais varrer o lixo para debaixo de um tapete. O código de depuração não é necessariamente limpo; e o código repleto de verificações ou tratamento de erros raramente é legível.
Regra 1: sempre há problemas no computador
O computador está com problemas e o programa travou durante a última execução.
O aplicativo deve primeiro verificar se ele inicia em um estado conhecido, bom e seguro antes de tentar fazer algo. Às vezes, simplesmente não há cópia do estado, porque o usuário o excluiu ou atualizou o computador. O programa travou durante a última execução e, paradoxalmente, durante a primeira execução também.
Por exemplo, ao ler ou gravar o estado em um arquivo, os seguintes problemas podem ocorrer:
- O arquivo está ausente.
- O arquivo está corrompido.
- Arquivo de uma versão mais antiga ou mais recente.
- A última alteração no arquivo não foi concluída.
- O sistema de arquivos está mentindo para você.
Esses problemas não são novos: os bancos de dados os encontram desde os tempos antigos (01-01 1970). Usar algo como SQLite ajudará a lidar com muitos problemas semelhantes, mas se o programa travar na última execução, o código poderá funcionar com dados errados e / ou de maneira incorreta.
Por exemplo, com programas agendados, algo desta lista acontecerá:
- O programa começará duas vezes em uma hora devido ao horário de verão.
- O programa será iniciado duas vezes porque o operador esqueceu que já está em execução.
- O programa começará tarde devido a falta de espaço livre em disco ou problemas misteriosos de nuvem ou rede.
- O programa será executado por mais de uma hora, o que pode levar a um atraso nas chamadas subseqüentes ao programa.
- O programa começará na hora errada do dia.
- O programa será inevitavelmente executado logo antes de algum horário de fronteira, por exemplo, meia-noite, final do mês ou ano, e falhará devido a erros computacionais.
A criação de software sustentável começa com a criação de um software que acha que caiu no tempo anterior e falha se você não souber o que fazer. A melhor coisa sobre lançar uma exceção e deixar um comentário no estilo de "isso não deveria acontecer" é que, quando isso acontece inevitavelmente, você terá uma vantagem inicial para depurar seu código.
O programa nem é obrigado a se recuperar de uma falha, é suficiente para permitir que desista e não agrave a situação. Pequenas verificações que geram exceções podem economizar semanas na espionagem, e um arquivo de bloqueio simples pode economizar horas na recuperação de backups.
O código fácil de depurar é:
- um código que verifica se está tudo bem antes de fazer o que eles pedem;
- código que facilita retornar a um estado conhecido e tente novamente;
- bem como código com níveis de segurança que causam erros o mais cedo possível.
Regra 2: Seu programa luta consigo mesmo
O maior ataque de DoS da história do Google veio de nós mesmos (porque nossos sistemas são muito grandes). Embora, de tempos em tempos, alguém tente nos testar a força, mas ainda assim somos capazes de nos prejudicar mais do que os outros.
Isso se aplica a todos os nossos sistemas.
Astrid Atkinson, engenheira de jogos longos
Os programas sempre travam durante a última execução; nem sempre há processador, memória ou espaço em disco suficiente. Todos os trabalhadores estão martelando em uma fila vazia, todos estão tentando repetir uma solicitação com falha e desatualizada e todos os servidores ao mesmo tempo pausam durante a coleta de lixo. O sistema não está apenas quebrado, está constantemente tentando se quebrar.
Grande dificuldade pode causar até mesmo a verificação do sistema.
A implementação de uma verificação de operação do servidor pode ser fácil, mas apenas se não processar solicitações. Se você não verificar a duração do tempo de atividade contínuo, é possível que o programa esteja entre verificações. Os erros também podem ser desencadeados por verificações de saúde: tive que escrever verificações que levaram à falha do sistema, que eles tiveram que proteger. Duas vezes, com diferença de três meses.
O código de tratamento de erros inevitavelmente levará à descoberta de ainda mais erros que precisam ser processados, muitos dos quais decorrem do próprio tratamento de erros. Da mesma forma, as otimizações de desempenho geralmente são a causa de gargalos no sistema. Um aplicativo agradável de usar em uma guia se transforma em um problema, sendo iniciado em 20 cópias.
Outro exemplo: um trabalhador em um pipeline executa muito rápido e consome memória disponível antes da próxima parte do pipeline acessá-lo. Isso pode ser comparado com engarrafamentos: eles surgem devido ao aumento da velocidade e, como resultado, o congestionamento do tráfego cresce na direção oposta. Portanto, as otimizações podem gerar sistemas que caem sob cargas altas ou pesadas, geralmente de maneiras misteriosas.
Em outras palavras: quanto mais rápido o sistema, mais forte será a pressão e, se você não permitir que o sistema neutralize um pouco, não se surpreenda se ele quebrar.
A contração é uma das formas de feedback do sistema. O programa, que é fácil de depurar, envolve o usuário no ciclo de feedback, permitindo que você veja todos os comportamentos dentro do sistema, aleatórios, intencionais, desejados e não desejados. Você pode inspecionar facilmente esse código, ver e entender as alterações que ocorrem com ele.
Regra 3: se você deixar algo ambíguo agora, precisará depurá-lo mais tarde
Em outras palavras, deve ser fácil rastrear as variáveis no programa e entender o que está acontecendo. Faça qualquer rotina com álgebra linear de pesadelo, você deve se esforçar para apresentar o estado do programa o mais óbvio possível. Isso significa que, no meio de um programa, você não pode alterar o objetivo de uma variável, pois o uso de uma variável para dois propósitos diferentes é um pecado mortal.
Isso também significa que você precisa evitar cuidadosamente o problema do semi-predicado, nunca use um único valor (
count
) para representar um par de valores (
boolean
,
count
). É necessário evitar retornar um número positivo para o resultado e, ao mesmo tempo, retornar
-1
se nada corresponder. O fato é que você pode facilmente se encontrar em uma situação em que precisa de algo como "
0, but true
" (e é exatamente isso que um recurso está no Perl 5); ou quando você cria um código difícil de combinar com outras partes do sistema (
-1
na próxima parte do programa pode não ser um erro, mas um valor de entrada válido).
Além de usar uma variável para duas finalidades, não é recomendável usar duas variáveis para a mesma finalidade, especialmente se for booleano. Não quero dizer que é ruim usar dois números para armazenar um intervalo, mas usar números booleanos para indicar o estado de um programa geralmente é uma máquina de estados mascarada.
Quando um estado não passa de cima para baixo, ou seja, no caso de um ciclo episódico, é melhor fornecer ao estado sua própria variável e limpar a lógica. Se você tiver um conjunto de booleanos dentro do objeto, substitua-os por uma variável chamada
state
e use enum (ou uma string, se necessário, em algum lugar).
if
expressões se parecerem com
if state == name
, não
if bad_name && !alternate_option
.
Mesmo se você criar uma máquina de estado explícita, há uma chance de confundir: às vezes o código pode ter duas máquinas de estado ocultas. Uma vez fui torturado a escrever proxies HTTP, até tornar cada máquina explícita, rastrear o status da conexão e analisá-lo separadamente. Quando você combina duas máquinas de estado em uma, pode ser difícil adicionar um novo estado ou entender exatamente qual o estado que algo deve ter.
É mais sobre a criação de código que não precisa ser depurado do que sobre depuração fácil. Se você desenvolver uma lista de estados corretos, será muito mais fácil descartar os incorretos sem perder acidentalmente um ou dois.
Regra 4: comportamento aleatório é o comportamento esperado.
Quando você não entende o que a estrutura de dados faz, essas lacunas de conhecimento são preenchidas pelos usuários: qualquer comportamento do código, intencional ou acidental, acabará por depender de algo. Muitas linguagens de programação populares oferecem suporte a tabelas de hash que podem ser iteradas e que, na maioria dos casos, mantêm a ordem após a inserção.
Em alguns idiomas, o comportamento da tabela de hash atende às expectativas da maioria dos usuários, repetindo as chaves na ordem em que foram adicionadas. Em outros idiomas, a tabela de hash em cada iteração retorna as chaves em uma ordem diferente. Nesse caso, alguns usuários reclamam que o comportamento
não é aleatório o
suficiente .
Infelizmente, qualquer fonte de aleatoriedade no seu programa acabará por ser usada para simulação estatística ou, pior ainda, criptografia; e qualquer fonte de classificação será usada para classificação.
Nos bancos de dados, alguns identificadores contêm um pouco mais de informações que outros. Ao criar uma tabela, o desenvolvedor pode escolher entre diferentes tipos de chave primária. A escolha correta é o UUID, ou algo indistinguível dele. A desvantagem de outras opções é que elas podem divulgar informações sobre pedidos e identificação. Ou seja, não apenas
a == b
, mas
a <= b
, e outras opções significam chaves de incremento automático.
Ao usar uma chave de incremento automático, o banco de dados atribui um número a cada linha da tabela, adicionando 1 ao inserir uma nova linha. E a classificação é vaga: as pessoas não sabem qual parte dos dados é canônica. Em outras palavras, você classifica por chave ou carimbo de data / hora? Como em uma tabela de hash, as próprias pessoas escolherão a resposta certa. E outro problema é que os usuários podem prever facilmente registros vizinhos com outras chaves.
Mas qualquer tentativa de superar o UUID falhará: já tentamos usar códigos postais, números de telefone e endereços IP, e sempre falhamos miseravelmente. Um UUID pode não facilitar a depuração do seu código, mas um comportamento menos aleatório significa menos problemas.
A partir das chaves, você pode extrair informações não apenas sobre pedidos. Se no banco de dados você criar chaves com base em outros campos, as pessoas descartarão os dados e os restaurarão da chave. E dois problemas surgirão: quando o estado do programa for armazenado em vários locais, será muito fácil as cópias discordarem entre si; e sincronizá-los será mais difícil se você não tiver certeza de qual precisa ser alterado ou qual foi alterado.
Tudo o que você permitir que seus usuários façam, eles o farão. Escrever um código fácil de depurar significa pensar em maneiras de usá-lo mal, bem como em como as pessoas podem interagir com ele em geral.
Regra 5: Depurar é uma tarefa social, antes de tudo, técnica.
Quando um projeto é dividido em componentes e sistemas, pode ser muito mais difícil encontrar bugs. Ao entender como o problema surge, você pode coordenar as alterações em diferentes partes para corrigir o comportamento. A correção de bugs em grandes projetos requer não apenas encontrá-los, mas convencer as pessoas da existência desses bugs ou da própria possibilidade de existência.
Existem erros no software, porque ninguém tem certeza absoluta de quem é responsável pelo quê. Ou seja, é mais difícil depurar o código quando nada está escrito, você precisa perguntar sobre tudo no Slack e ninguém responde até que um especialista apareça.
Isso pode ser corrigido com planejamento, ferramentas, processos e documentação.
O planejamento é uma maneira de se livrar do estresse de manter contato, a estrutura de gerenciamento de incidentes. Os planos permitem informar os compradores, liberar pessoas que estão em contato há muito tempo, além de rastrear problemas e fazer alterações para reduzir riscos futuros. Ferramentas - uma maneira de reduzir os requisitos para a execução de algum trabalho, para que fique mais acessível a outros desenvolvedores. Um processo é uma maneira de remover funções de gerenciamento de participantes individuais e passá-las para uma equipe.
As pessoas e os modos de interação mudarão, mas os processos e as ferramentas permanecerão à medida que a equipe se transformar. Não é que um seja mais importante que o outro, mas que um seja projetado para suportar mudanças no outro. O processo também pode ser usado para remover as funções de controle da equipe. Isso nem sempre é bom ou ruim, mas sempre há
algum tipo de processo, mesmo que não seja explicitado. E o ato de documentá-lo é o primeiro passo para permitir que outras pessoas mudem esse processo.
A documentação é mais do que arquivos de texto. Essa é uma maneira de transferir responsabilidade, como você leva as pessoas para o trabalho, como relata mudanças para os afetados por essas mudanças. Escrever documentação requer mais empatia do que quando se escreve código e mais habilidades: não há sinalizadores de compilador simples ou verificações de tipo, e você pode escrever facilmente muitas palavras sem documentar nada.
Sem documentação, não se pode esperar que outros tomem decisões informadas, ou mesmo concordar com as conseqüências do uso do software. Sem documentação, ferramentas ou processos, é impossível compartilhar o ônus da manutenção ou, pelo menos, substituir as pessoas que estão resolvendo o problema.
O desejo de facilitar a depuração é aplicável não apenas ao código em si, mas também aos processos relacionados ao código, isso ajuda a entender em qual skin você precisa entrar para corrigir o código.
Fácil de depurar código é fácil de explicar.
Há uma opinião de que, se você explicar um problema para alguém durante a depuração, você mesmo o entenderá. Para isso, você nem precisa de outra pessoa, o principal é se forçar a explicar a situação do zero, a explicar a ordem de reprodução. E muitas vezes isso é suficiente para chegar à decisão certa.
Se apenas. Às vezes, quando pedimos ajuda, não perguntamos o que é necessário. Isso é tão comum que se chama The XY Problem: “
Como obtenho as últimas três letras de um nome de arquivo? Hein? Não, eu quis dizer expansão . ”
Falamos sobre um problema em termos de uma solução que entendemos e falamos sobre uma solução em termos das consequências que tememos. Depurar é uma compreensão difícil de conseqüências inesperadas e soluções alternativas; requer o mais difícil para um programador: admitir que ele entendeu algo errado.
Acontece que este não foi um erro do compilador.