Os seis mandamentos do bom código: escreva um código que resista ao teste do tempo

Publicados: 2022-03-11

Os seres humanos só lutam com a arte e a ciência da programação de computadores há cerca de meio século. Comparada com a maioria das artes e ciências, a ciência da computação ainda é, em muitos aspectos, apenas uma criança pequena, andando em paredes, tropeçando nos próprios pés e ocasionalmente jogando comida na mesa.

Como consequência de sua relativa juventude, não acredito que ainda tenhamos um consenso sobre o que é uma definição adequada de “bom código”, pois essa definição continua a evoluir. Alguns dirão que “bom código” é um código com 100% de cobertura de teste. Outros dirão que é super rápido e tem um desempenho matador e funcionará de maneira aceitável em hardware de 10 anos.

Embora esses sejam todos objetivos louváveis ​​para desenvolvedores de software, arrisco-me a lançar outro alvo na mistura: a manutenção. Especificamente, “bom código” é um código que é fácil e prontamente mantido por uma organização (não apenas por seu autor!) e que viverá por mais tempo do que apenas o sprint em que foi escrito. carreira como engenheiro em grandes e pequenas empresas, nos EUA e no exterior, que parecem se correlacionar com softwares “bons” e sustentáveis.

Nunca se contente com o código que apenas "funciona". Escreva código superior.
Tweet

Mandamento nº 1: Trate seu código da maneira que você deseja que o código dos outros o trate

Estou longe de ser a primeira pessoa a escrever que o público principal do seu código não é o compilador/computador, mas quem quer que tenha que ler, entender, manter e aprimorar o código (que não será necessariamente você daqui a seis meses ). Qualquer engenheiro que valha a pena pode produzir código que “funciona”; o que distingue um engenheiro excelente é que ele pode escrever código sustentável de forma eficiente que suporta um negócio a longo prazo e tem a habilidade de resolver problemas de forma simples e clara e sustentável.

Em qualquer linguagem de programação, é possível escrever código bom ou código ruim. Supondo que julguemos uma linguagem de programação por quão bem ela facilita a escrita de um bom código (deveria ser pelo menos um dos principais critérios, de qualquer maneira), qualquer linguagem de programação pode ser “boa” ou “ruim” dependendo de como é usada (ou abusada). ).

Um exemplo de uma linguagem que por muitos é considerada “limpa” e legível é o Python. A linguagem em si impõe algum nível de disciplina de espaço em branco e as APIs incorporadas são abundantes e bastante consistentes. Dito isto, é possível criar monstros indescritíveis. Por exemplo, pode-se definir uma classe e definir/redefinir/desdefinir todo e qualquer método nessa classe durante o tempo de execução (geralmente chamado de patch de macaco). Essa técnica naturalmente leva, na melhor das hipóteses, a uma API inconsistente e, na pior, a um monstro impossível de depurar. Alguém pode ingenuamente pensar “claro, mas ninguém faz isso!” Infelizmente, eles fazem isso, e não demora muito para navegar em pypi antes de você encontrar bibliotecas substanciais (e populares!) que (ab)usam patches de macaco extensivamente como o núcleo de suas APIs. Recentemente, usei uma biblioteca de rede cuja API inteira muda dependendo do estado de rede de um objeto. Imagine, por exemplo, chamar client.connect() e às vezes obter um erro MethodDoesNotExist em vez de HostNotFound ou NetworkUnavailable .

Mandamento nº 2: Um bom código é facilmente lido e entendido, em parte e no todo

Um bom código é facilmente lido e compreendido, em parte e no todo, por outros (assim como pelo autor no futuro, tentando evitar a síndrome “Eu realmente escrevi isso?” ).

Por “em parte” quero dizer que, se eu abrir algum módulo ou função no código, devo ser capaz de entender o que ele faz sem ter que ler também todo o resto da base de código. Deve ser o mais intuitivo e autodocumentado possível.

Código que constantemente faz referência a detalhes minuciosos que afetam o comportamento de outras partes (aparentemente irrelevantes) da base de código é como ler um livro onde você tem que referenciar as notas de rodapé ou um apêndice no final de cada frase. Você nunca passaria da primeira página!

Alguns outros pensamentos sobre legibilidade “local”:

  • Um código bem encapsulado tende a ser mais legível, separando as preocupações em todos os níveis.

  • Os nomes importam. Ative o sistema de Pensamento Rápido e Devagar 2 como o cérebro forma pensamentos e coloca algum pensamento real e cuidadoso em nomes de variáveis ​​e métodos. Os poucos segundos extras podem render dividendos significativos. Uma variável bem nomeada pode tornar o código muito mais intuitivo, enquanto uma variável mal nomeada pode levar a falsificações e confusão.

  • A inteligência é o inimigo. Ao usar técnicas, paradigmas ou operações extravagantes (como compreensões de lista ou operadores ternários), tenha o cuidado de usá-los de uma maneira que torne seu código mais legível, não apenas mais curto.

  • A consistência é uma coisa boa. A consistência no estilo, tanto em termos de como você coloca chaves, mas também em termos de operações, melhora muito a legibilidade.

  • Separação de preocupações. Um determinado projeto gerencia um número incontável de suposições localmente importantes em vários pontos da base de código. Exponha cada parte da base de código ao menor número possível dessas preocupações. Digamos que você tenha um sistema de gerenciamento de pessoas em que um objeto de pessoa às vezes pode ter um sobrenome nulo. Para alguém escrevendo código em uma página que exibe objetos de pessoa, isso pode ser muito estranho! E a menos que você mantenha um manual de “Suposições estranhas e não óbvias que nossa base de código tem” (eu sei que não) seu programador de página de exibição não saberá que os sobrenomes podem ser nulos e provavelmente escreverá código com um ponteiro nulo exceção nele quando o caso nulo do sobrenome aparece. Em vez disso, lide com esses casos com APIs e contratos bem pensados ​​que diferentes partes de sua base de código usam para interagir umas com as outras.

Mandamento nº 3: Um bom código tem um layout e uma arquitetura bem pensados ​​para tornar o gerenciamento do estado óbvio

O Estado é o inimigo. Por quê? Porque é a parte mais complexa de qualquer aplicativo e precisa ser tratada de forma muito deliberada e ponderada. Os problemas comuns incluem inconsistências de banco de dados, atualizações parciais da interface do usuário em que os novos dados não são refletidos em todos os lugares, operações fora de ordem ou apenas códigos complexos com instruções if e ramificações em todos os lugares, levando a códigos difíceis de ler e ainda mais difíceis de manter. Colocar o estado em um pedestal para ser tratado com muito cuidado e ser extremamente consistente e deliberado em relação a como o estado é acessado e modificado simplifica drasticamente sua base de código. Algumas linguagens (Haskell, por exemplo) impõem isso em um nível programático e sintático. Você ficaria surpreso com o quanto a clareza de sua base de código pode melhorar se você tiver bibliotecas de funções puras que não acessam nenhum estado externo e, em seguida, uma pequena área de superfície de código com estado que faz referência à funcionalidade pura externa.

Mandamento #4: O bom código não reinventa a roda, fica nos ombros dos gigantes

Antes de potencialmente reinventar uma roda, pense em quão comum é o problema que você está tentando resolver ou a função que você está tentando executar. Alguém pode já ter implementado uma solução que você pode aproveitar. Reserve um tempo para pensar e pesquisar essas opções, se apropriado e disponível.

Dito isso, um contra-argumento completamente razoável é que as dependências não vêm “de graça” sem nenhuma desvantagem. Ao usar uma biblioteca de terceiros ou de código aberto que adiciona algumas funcionalidades interessantes, você está se comprometendo e se tornando dependente dessa biblioteca. Esse é um grande compromisso; se for uma biblioteca gigante e você precisar apenas de uma pequena funcionalidade, você realmente quer o ônus de atualizar toda a biblioteca se atualizar, por exemplo, para o Python 3.x? Além disso, se você encontrar um bug ou quiser aprimorar a funcionalidade, você depende do autor (ou fornecedor) para fornecer a correção ou aprimoramento ou, se for de código aberto, encontra-se na posição de explorar um ( potencialmente substancial) você não está familiarizado com a tentativa de corrigir ou modificar um pouco de funcionalidade obscura.

Certamente, quanto mais bem usado for o código do qual você depende, menos provável será que você tenha que investir tempo em manutenção. A conclusão é que vale a pena fazer sua própria pesquisa e avaliar se deve ou não incluir tecnologia externa e quanta manutenção essa tecnologia específica adicionará à sua pilha.

Abaixo estão alguns dos exemplos mais comuns de coisas que você provavelmente não deveria reinventar na era moderna em seu projeto (a menos que estes SEJAM seus projetos).

Bancos de dados

Descubra qual CAP você precisa para o seu projeto e escolha o banco de dados com as propriedades certas. Banco de dados não significa mais apenas MySQL, você pode escolher entre:

  • SQL Schema'ed “tradicional”: Postgres / MySQL / MariaDB / MemSQL / Amazon RDS, etc.
  • Armazenamentos de valores-chave: Redis / Memcache / Riak
  • NoSQL: MongoDB/Cassandra
  • Bancos de dados hospedados: AWS RDS / DynamoDB / AppEngine Datastore
  • Trabalho pesado: Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
  • Coisas loucas: Mnesia de Erlang, dados principais do iOS

Camadas de abstração de dados

Você deve, na maioria das circunstâncias, não escrever consultas brutas para qualquer banco de dados que você escolheu usar. É mais provável que exista uma biblioteca entre o banco de dados e o código do aplicativo, separando as preocupações de gerenciamento de sessões simultâneas de banco de dados e detalhes do esquema de seu código principal. No mínimo, você nunca deve ter consultas brutas ou SQL inline no meio do código do seu aplicativo. Em vez disso, envolva-o em uma função e centralize todas as funções em um arquivo chamado algo realmente óbvio (por exemplo, “queries.py”). Uma linha como users = load_users() , por exemplo, é infinitamente mais fácil de ler do que users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”) . Esse tipo de centralização também torna muito mais fácil ter um estilo consistente em suas consultas e limita o número de lugares para alterar as consultas caso o esquema seja alterado.

Outras bibliotecas e ferramentas comuns a serem consideradas

  • Serviços de enfileiramento ou Pub/Sub. Faça sua escolha de provedores AMQP, ZeroMQ, RabbitMQ, Amazon SQS
  • Armazenar. Amazon S3, armazenamento em nuvem do Google
  • Monitoramento: Graphite/Hosted Graphite, AWS Cloud Watch, New Relic
  • Coleta/Agregação de Logs. Loggly, Splunk

Escalonamento automático

  • Escalonamento Automático. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Digital Ocean

Mandamento nº 5: Não atravesse os riachos!

Existem muitos bons modelos para design de programação, pub/sub, atores, MVC etc. Escolha o que você mais gosta e siga-o. Diferentes tipos de lógica que lidam com diferentes tipos de dados devem ser fisicamente isolados na base de código (mais uma vez, essa separação de conceitos de preocupações e redução da carga cognitiva no futuro leitor). O código que atualiza sua interface do usuário deve ser fisicamente distinto do código que calcula o que entra na interface do usuário, por exemplo.

Mandamento nº 6: Quando possível, deixe o computador fazer o trabalho

Se o compilador puder detectar erros lógicos em seu código e evitar mau comportamento, bugs ou travamentos, devemos tirar vantagem disso. Claro, algumas linguagens têm compiladores que tornam isso mais fácil do que outras. Haskell, por exemplo, tem um compilador notoriamente estrito que resulta em programadores gastando a maior parte de seu esforço apenas para compilar o código. Uma vez compilado, porém, “simplesmente funciona”. Para aqueles que nunca escreveram em uma linguagem funcional fortemente tipada, isso pode parecer ridículo ou impossível, mas não acredite na minha palavra. Sério, clique em alguns desses links, é absolutamente possível viver em um mundo sem erros de execução. E é realmente tão mágico.

É certo que nem toda linguagem tem um compilador ou uma sintaxe que se presta a muita (ou, em alguns casos, nenhuma!) verificação em tempo de compilação. Para aqueles que não têm, reserve alguns minutos para pesquisar quais verificações opcionais de rigidez você pode habilitar em seu projeto e avaliar se elas fazem sentido para você. Uma lista curta e não abrangente de alguns comuns que usei recentemente para linguagens com tempos de execução brandos incluem:

  • Python: pylint, pyflakes, avisos, avisos em emacs
  • Rubi: avisos
  • JavaScript: jslint

Conclusão

Esta não é de forma alguma uma lista exaustiva ou perfeita de mandamentos para produzir um código “bom” (ou seja, de fácil manutenção). Dito isso, se toda base de código que eu tiver que pegar no futuro seguisse metade dos conceitos desta lista, terei muito menos cabelos grisalhos e talvez até consiga adicionar cinco anos extras no final da minha vida. E certamente acharei o trabalho mais agradável e menos estressante.