Escreva código fácil de apagar, não código difícil de expandir (2016)
(programmingisterrible.com)- No momento em que é escrito, o código já gera custo de manutenção, então muitas vezes é mais importante ter uma estrutura fácil de apagar ou substituir depois do que priorizar reusabilidade
- Quanto mais usuários uma API tiver, maior será o custo de mudá-la; e quanto mais profunda for a dependência de APIs de terceiros, mais o codebase será abalado por mudanças externas
- Duplicação, boilerplate, camadas, blocos grandes de código, separação em módulos e feature flags podem todos se tornar ferramentas de gestão de dependências, dependendo do contexto
- Uma boa separação não é tanto agrupar funcionalidades em comum, mas sim esconder entre si decisões de projeto que são difíceis de mudar ou têm grande chance de mudar
- Bom código não é código perfeito desde o início, mas código legado que atrapalha menos com o passar do tempo e, no fim das contas, código fácil de apagar
Código é custo, e apagar é reduzir custo
- Todo código, no instante em que é escrito, cria custo de manutenção; reutilização pode reduzir a quantidade de código, mas também pode dificultar mudar de ideia depois
- Quanto mais código usa uma API, maior o custo de reescrita quando essa API muda
- Quanto mais você depende de APIs de terceiros, maior o impacto quando elas mudam
- Em sistemas grandes, como o código se encaixa e de que partes outras partes dependem se torna um problema cada vez mais difícil com o tempo
- Se você olhar linhas de código não como “linhas produzidas”, mas como “custo acumulado”, apagar código passa a ser uma forma de reduzir custo de manutenção
- O objetivo não é só construir software reutilizável, mas software descartável
Etapa 0: não escrever código
- Linhas de código por si só não dizem tudo, mas a escala importa: 50 linhas, 500 linhas, 5.000 linhas, 10.000 linhas, 25.000 linhas
- Um monólito de 1 milhão de linhas exige mais tempo, custo e esforço para substituir do que um monólito de 10 mil linhas
- Quanto mais código existe, mais difícil fica eliminá-lo, mas reduzir uma única linha quase não economiza nada
- O código mais fácil de apagar é, desde o começo, o código que você não escreveu
Etapa 1: copiar e colar
- Código reutilizável costuma ser mais fácil de criar depois que vários casos de uso reais aparecem, em vez de tentar prever usos futuros e construí-lo antes
- Fazer copy-paste algumas vezes dentro do codebase ajuda a entender como o código realmente é usado
- No momento em que você transforma algo em uma API compartilhada, esse código fica mais difícil de mudar
- Código que chama uma função acaba dependendo não só do comportamento documentado, mas também de comportamentos intencionais e não intencionais observados na implementação
- Apagar o código dentro de uma função é mais simples do que apagar a própria função
Etapa 2: parar de copiar e colar
- Quando um padrão já se repetiu o suficiente, chega a hora de extraí-lo para uma função
- Entram aqui códigos de natureza utilitária que costumam ser necessários sobre a biblioteca padrão, como abrir um arquivo de configuração e retornar uma hash table, ou apagar um diretório
- É melhor manter
utilcomo um diretório, não como um único arquivo, e colocar utilitários diferentes em arquivos diferentes- Um único arquivo
utiltende a crescer sem parar e, quando fica grande demais, se torna difícil de dividir
- Um único arquivo
- Código menos específico de uma aplicação ou projeto é mais fácil de reutilizar e tem menos chance de mudar ou ser apagado
- Isso inclui código de biblioteca para logging, APIs de terceiros, file handles e processos
- Listas, hash tables e coleções sobrevivem bem não só por terem interfaces simples, mas porque seu escopo não tende a crescer com o tempo
- O ponto principal é manter as partes difíceis de apagar o mais longe possível das partes fáceis de apagar
Etapa 3: usar mais boilerplate
- Criar bibliotecas evita copy-paste, mas na prática costuma exigir muito boilerplate para usá-las
- Boilerplate se parece com copy-paste no sentido de que, toda vez, você altera pequenos detalhes em lugares diferentes
- Esse tipo de duplicação aceita verbosidade em troca de menos dependências e mais flexibilidade
- Bibliotecas que exigem boilerplate costumam aparecer em casos em que é difícil misturar política e protocolo, como protocolos de rede, wire formats e ferramentas de parsing
- Protocolo trata do que um programa pode fazer
- Política trata do que um programa deve fazer
- Esse tipo de código muitas vezes existe por exigências de comunicação com outros computadores ou manipulação de arquivos, então é difícil de apagar
- É importante não espalhar a lógica de negócio por esse código
- Mesmo escrevendo mais linhas, é melhor gastá-las em partes que sejam fáceis de apagar
Etapa 4: não escrever boilerplate
- Quando o boilerplate cresce demais, é hora de criar uma biblioteca opinativa sobre política, workflow e estado que envolva uma biblioteca flexível
- Criar uma API fácil de usar é quase o mesmo que transformar boilerplate em biblioteca
- O cliente HTTP para Python
requestsé um exemplo de interface simples construída sobre o mais verbosourllib3requestslida com workflows comuns de uso de HTTP e esconde detalhes práticosurllib3oferece pipelining, gerenciamento de conexões etc., sem esconder esses detalhes do usuário
- Envolver uma biblioteca com outra não é apenas esconder detalhes, mas também separar responsabilidades
- É melhor não colocar lógica de negócio no diretório
util, e sim empilhar bibliotecas fáceis de usar sobre bibliotecas fáceis de implementar - Às vezes também vale a pena envolver bibliotecas de terceiros
- Assim o projeto inteiro não fica preso a uma escolha específica, e você pode ter uma biblioteca adaptada ao seu próprio código
- Uma API agradável de usar e uma API expansível frequentemente entram em conflito
- Camadas têm menos a ver com escrever código para apagar depois e mais com tornar código difícil de apagar utilizável sem contaminar a lógica de negócio
Etapa 5: escrever grandes blocos de código
- Mesmo com copy-paste, refatoração, camadas e composição, o código ainda precisa fazer alguma coisa, então às vezes é necessário um grande bloco de código que segure o resto
- Lógica de negócio pode ser marcada por uma infinidade de edge cases e hacks rápidos
- Código de jogos ou código escrito por fundadores também pode ser visto como esse mesmo tipo de código que escolhe atalhos para economizar bastante tempo
- Às vezes é mais fácil apagar um grande erro do que eliminar 18 pequenos erros entrelaçados
- Como muita programação é exploratória, pode ser mais rápido errar algumas vezes e iterar do que tentar acertar de primeira
- Ao criar seu primeiro jogo, é melhor não começar pelo engine; e antes de escrever uma aplicação, também pode não valer a pena começar criando um framework web
- Monorepo é um trade-off parecido
- É difícil saber de antemão como dividir o código, e um erro grande fortemente acoplado pode ser mais fácil de implantar do que 20 erros menores
- Se você sabe que o código logo será descartado, apagado ou facilmente substituído, pode se permitir mais atalhos
- O objetivo não é repetir dez vezes o mesmo monte de barro até aperfeiçoar o erro, mas iterar cometendo erros novos e assumindo riscos novos a cada vez
- No fim, projetos fracassam ou viram código legado, e fracassos acontecem mais do que sucessos
- Muitas vezes é mais fácil apagar o sistema inteiro do que apagar código peça por peça
Etapa 6: dividir o código em partes
- O grande monte de barro é o mais fácil de criar, mas também o mais caro de manter
- Uma mudança que parece simples pode acabar exigindo remendos em quase todo o codebase
- Um código que era fácil de apagar como um todo pode se tornar difícil de apagar quando visto em partes
- É melhor dividir módulos não por funcionalidade comum, mas pelo que não deve ser compartilhado com o restante e pelas decisões de projeto que precisam ser escondidas
- Como no critério de D. Parnas, você pode listar decisões de projeto difíceis ou com grande chance de mudar e desenhar cada módulo para escondê-las dos demais
- Módulos não existem para reuso, mas para possibilidade de mudança
- O princípio da responsabilidade única pode ser visto como “cada módulo deve lidar com apenas um problema difícil”, mas o mais importante é “cada problema difícil deve ser tratado em apenas um módulo”
- Se um módulo faz duas coisas, muitas vezes isso significa que mudar uma parte exige mudar a outra também
- Um componente horrível com interface simples pode ser mais fácil do que dois componentes que exigem coordenação cuidadosa
Acoplamento frouxo e interfaces comuns
- Sistemas em que você pode apagar uma parte sem reescrever o resto costumam ser chamados de frouxamente acoplados
- Acoplamento frouxo se aproxima de um estado em que mudar de ideia não obriga você a alterar código demais
- Até hardcodar uma variável uma vez, ou usar uma flag de linha de comando em vez de uma variável, pode em certos casos virar uma forma de acoplamento frouxo
- O Microsoft Windows alcança esse objetivo separando APIs externas e internas
- As APIs externas ficam ligadas ao ciclo de vida de programas desktop
- As APIs internas ficam ligadas ao kernel subjacente
- Esconder APIs permite ganhar flexibilidade sem quebrar uma grande quantidade de software
- HTTP também oferece um exemplo de acoplamento frouxo
- Você pode colocar um cache na frente de um servidor HTTP
- É possível mover imagens para uma CDN e mudar só os links sem quebrar o navegador
- Códigos de erro HTTP dão códigos específicos a problemas comuns, permitindo que clientes tratem muitos erros por conta própria
- A forma de tratar falhas deve ser considerada junto com a divisão do código em partes menores
Tratamento de falhas e acoplamento
- Erlang/OTP usa uma abordagem relativamente única para lidar com falhas por meio de árvores de supervisão
- Em sistemas Erlang, cada processo normalmente é iniciado e monitorado por um supervisor
- Se algo dá errado com o processo, ele é encerrado
- Quando o processo termina, o supervisor o reinicia
- O supervisor é iniciado por um bootstrap process e, se o supervisor falha, esse bootstrap process o reinicia
- A ideia central é que muitas vezes é mais rápido falhar e reiniciar do que tratar o erro
- Falhas transitórias às vezes podem ser contidas com a abordagem de desligar e ligar de novo
- Tratamento de erros e recuperação costumam funcionar melhor nas camadas mais externas do codebase, o que é conhecido como end-to-end principle
- É mais fácil tratar falhas nas extremidades do que no meio da conexão, e mesmo que haja tratamento interno, ainda é necessária uma verificação no topo
- Tratamento de erros é uma das várias formas de acoplamento forte em um sistema
IMAP, sistemas de arquivos, SQL e middleware
- IMAP é um caso excepcional em que quase toda operação tem opções e tratamentos próprios, tornando o tratamento de erro doloroso
- No IMAP, erros podem surgir no meio do resultado de outras operações
- Em vez de UUID, ele cria tokens únicos para identificar cada mensagem, e esses tokens podem mudar no meio do resultado de uma operação
- Muitas operações de IMAP não são atômicas
- Levou mais de 25 anos para surgir uma forma confiável de mover e-mails de uma pasta para outra
- Também existem uma codificação especial em UTF-7 e uma codificação base64 própria
- Sistemas de arquivos e bancos de dados servem como comparações melhores para armazenamento remoto
- Sistemas de arquivos têm um conjunto fixo de operações e vários objetos
- SQL parece ter uma interface mais ampla do que um sistema de arquivos, mas segue o padrão de várias operações sobre conjuntos e várias linhas
- Nem sempre é possível trocar bancos de dados entre si, mas ainda é mais fácil encontrar algo que funcione com SQL do que com uma linguagem de consulta criada por você mesmo
- O Finagle, do Twitter, usa uma API comum para serviços, facilitando adicionar tratamento de timeout, mecanismos de retry e verificações de autenticação ao código de cliente e servidor
- Bons exemplos de acoplamento frouxo também costumam ser exemplos de interface uniforme
- Um codebase saudável não precisa ser perfeitamente modularizado, mas deve haver distância suficiente entre as partes móveis
- Código frouxamente acoplado não é necessariamente fácil de apagar, mas é muito mais fácil de substituir e mudar
Etapa 7: continuar escrevendo código
- Quando você consegue escrever código novo sem precisar lidar com código antigo, fica muito mais fácil experimentar ideias novas
- A questão central não é microservices versus monólito, mas conseguir colocar um ou dois experimentos sobre o sistema enquanto você ainda está entendendo o que ele deve fazer
- Feature flags são uma forma de permitir que você mude de ideia depois
- Feature flags servem não só para experimentar funcionalidades, mas também para implantar mudanças sem redistribuir o software
- O Google Chrome descobriu que a parte mais difícil de seu ciclo regular de releases era o tempo necessário para mesclar branches de funcionalidades de longa duração
- Se você pode ligar e desligar código novo sem recompilar, consegue dividir mudanças grandes em merges menores e evitar impacto no código existente
- Quando novas funcionalidades aparecem mais cedo no mesmo codebase, fica mais claro o efeito do desenvolvimento longo dessas funcionalidades sobre as outras partes
- Feature flags não são apenas switches simples de linha de comando, mas uma forma de separar o release de funcionalidades da mesclagem de branches e do deploy de código
- Quando implantar software novo pode levar horas, dias ou semanas, a capacidade de mudar de ideia em runtime se torna ainda mais importante
Bom código é código legado que não atrapalha
- Mais importante do que o fato de iterar é ter um ciclo de feedback
- Em vez de criar módulos para reuso, o essencial é isolar componentes para permitir mudanças
- Responder a mudanças inclui não só desenvolver novas funcionalidades, mas também remover funcionalidades antigas
- Escrever código expansível é torcer para que a escolha inicial ainda pareça certa daqui a três meses
- Código apagável parte da suposição oposta
- Camadas, isolamento, interfaces comuns e composição são formas de criar não um software “bom” em si, mas um software que possa mudar com o tempo
- Você não precisa jogar tudo fora, mas precisa apagar algumas partes
- Bom código não é o código que acertou tudo de primeira, mas código legado que não atrapalha
- Bom código é código fácil de apagar
1 comentários
Comentários do Hacker News
Uma frase de que gosto é simplicidade é robustez
É parecida com a lei da mudança contínua de Lehman: quanto menor a complexidade de um sistema, mais fácil ele é de mudar
Em vez de escrever código extensível pensando no futuro, acho melhor se preparar para o futuro com código intuitivo
Por exemplo, abstrair só quando for realmente necessário, permitir alguma duplicação simples, começar com um monólito no início e priorizar escala vertical antes de escala horizontal
Já criei vários sistemas 0→1, e o padrão em comum foi sempre esse
https://en.m.wikipedia.org/wiki/Lehman%27s_laws_of_software_...
O código não fica robusto só por ignorar casos de borda, mesmo que pareça mais simples por causa disso
Isso abre uma discussão sobre o que é “simplicidade” e como ela se aplica a sistemas, o que já é uma pergunta complexa o suficiente para Rich Hickey tratar
Talvez “o simples demais é robusto” ou “o direto é robusto” expressem melhor a intenção
Basta escrever código que faça o trabalho necessário. Não invente problemas hipotéticos de escala, não crie abstrações espertinhas só para parecer inteligente, escreva um monólito e suba numa VM que ele já pode entrar em produção
Se aparecer um problema, resolva naquele momento; de preferência depois que o fluxo de caixa já estiver positivo
Por que uma startup “AirBnb para cachorros” com 0 usuários está preocupada com C100K? Quando a AWS te convenceu a pagar por serverless, foi para o seu benefício ou para tirar mais dinheiro de você?
Posts relacionados:
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=24989351 - Nov 2020 (30 comments)
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=23914486 - July 2020 (109 comments)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=18761739 - Dec 2018 (2 comments)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=11093733 - Feb 2016 (133 comments)
Resumindo brevemente os erros que cometi quando era jovem, hoje acredito no oposto: projetar para apagar
Antes eu achava que podia prever todas as situações e criar uma bela obra de arte que atendesse a todos os requisitos. Mas ninguém consegue prever requisitos futuros tão bem assim
Em algum momento, aquilo que eu fiz vai virar “aquela idiotice” para outra pessoa, e por mais orgulho que eu tenha agora, pode ser perfeitamente justificável que destruam tudo
Por isso, é melhor investir energia em tornar algo fácil de remover. Isso muitas vezes reduz o acoplamento, mas o ponto importante é que isso é diferente do desacoplamento do jovem desenvolvedor entusiasmado que quer separar tudo em um framework com meta-configuração
Às vezes, um forte acoplamento fácil de entender é melhor
https://news.ycombinator.com/item?id=41219130
Por exemplo, surgem coisas como CommonExcelFileParser, CommonExcelFileParserUtilities, HasExcelParseStatus, ProductImportExcelParser, ProductImportExcelParserView, ProductImportExcelParserResultHandler, e elas acabam virando a base do código ao redor
É parecido com começar um projeto de frontend com React ou Angular e transformar a migração para outra coisa em uma tarefa de Sísifo
Na prática, as pessoas acabam construindo a plataforma inteira e, mesmo que existam escolhas que vão causar problemas no futuro, o acoplamento torna a refatoração muito mais difícil do que em um codebase pouco abstraído
Parece que as pessoas gostam mais de fazer isso do que de aplicar KISS e YAGNI e criar código fácil de apagar, então não sei bem o que fazer nesses casos
Requisitos de negócio mudam e se movem, então você não deve tentar prevê-los; deve escrever algo fácil de substituir ou descartar
Frameworks e bibliotecas são um pouco diferentes. Eles também precisam acompanhar as mudanças do mundo, mas podem fazer isso em um ritmo bem mais moderado
O maior problema é quando desenvolvedores em aplicações de negócios que já usam frameworks como Rails ou Asp.Net ainda querem criar outro “framework” por cima
Se você não está escrevendo o kernel do Linux, não deveria programar como se estivesse escrevendo o kernel do Linux
É bem estranho que este texto não trate nem um pouco de testes e observabilidade
Testes também têm custo de manutenção, mas reduzem o risco de quebrar algo quando você remove alguma coisa
Além disso, se você expôs um serviço a chamadores externos, precisa tanto de uma forma robusta de marcar algumas chamadas como obsoletas para removê-las depois quanto de uma maneira de observar se ainda estão sendo chamadas e por quem
Recentemente fizemos pela primeira vez uma remoção semiautomatizada de resolvers GraphQL expostos; já tínhamos métricas sobre a frequência de uso de determinados resolvers, então foi possível analisá-las para obter uma lista de resolvers que não podiam ser removidos
O GraphQL já tem a anotação
deprecated, mas nosso serviço não dava nenhum tratamento especial a essa anotaçãoEntão adicionamos observabilidade para sinalizar quando uma função obsoleta era chamada e, depois de rodar isso em produção por tempo suficiente, passamos a conseguir remover com segurança código exposto externamente
Quando você começa a tornar as coisas complexas demais, tudo vira uma bagunça interligada, e os desenvolvedores deixam de saber qual impacto uma mudança terá
Claro, há muitas formas de estragar isso. Você pode seguir princípios estúpidos de “boas práticas” ou fazer “microserviços” de um jeito em que ninguém sabe quem consome qual serviço. Mas aí você não construiu algo fácil de remover
O consumo externo é um bom exemplo. Faz sentido dar aos consumidores um aviso razoável sobre a descontinuação de um serviço, mas, se você não consegue realmente desligá-lo quando quiser, então o sistema não foi projetado para ser fácil de remover
Se essa abordagem for a certa, tudo bem. Só que esperar que testes e observabilidade avisem quando algo quebrar provavelmente não vai funcionar muito bem
Não sou contra testes em si, mas é difícil dizer que eles são uma proteção realmente excelente para informar se você quebrou algo em uma cadeia longa e complexa. Também é muito difícil ter cobertura de testes que realmente proteja nisso
Se você apagar parte do código, também poderá apagar parte dos testes
Dá para ler o texto como falando apenas de código, com os impactos relacionados sobre testes implicitamente incluídos
O fato de o texto não mencionar testes não significa que esteja dizendo para não escrever testes
Quando leio esta parte, sinto que o título nem sempre está certo: código fácil de remover em geral também é código fácil de expandir
Porque ele é organizado em camadas, modular e isola partes diferentes por meio de abstrações como interfaces ou outros contratos de tipo
Eu costumava dizer aos alunos de física computacional que o melhor cálculo é aquele que você não precisa fazer
Pessoalmente, separo o código em duas partes: lógica de negócio e implementação de fato
A lógica de negócio pode, por natureza, acabar duplicada, mas os detalhes técnicos não deveriam se repetir demais
A implementação real pode ser tão bagunçada quanto for necessário, desde que não contenha diretamente a lógica de negócio e permaneça independente da aplicação
Fazendo isso, quando você percebe que algo está uma bagunça e não funciona direito, em vez de forçar um conserto rastreando a especificação real a partir da implementação, você passa a ter a opção de simplesmente apagar a implementação inteira
A frase do primeiro parágrafo — “o problema da reutilização de código é que ela atrapalha mudar de ideia depois” — é claramente um erro
De modo geral, está errada. Se você mudou de ideia e o código foi copiado e colado em dez lugares, terá de corrigir os dez
Se, por outro lado, ele está dentro de uma função, basta mudar uma vez. Mesmo que depois você descubra que uma das dez chamadas não deveria mudar, aí você pode copiar e colar naquele ponto ou generalizar mais a função
Assim como atravessar a rua sem olhar, copiar e colar quase sempre é uma má ideia
Mas uma abstração ruim leva a meses de pagamento de dívida técnica
Claro, a resposta é “não crie abstrações ruins”, mas todos sabemos como isso funciona em equipes e com requisitos de produto em mudança
Temos um submódulo git com widgets de UI comuns, mas agora mudar um deles está quase impossível; é mais fácil copiar o componente para dentro do projeto e alterá-lo localmente
Isso é um problema. Código compartilhado deveria ser o mínimo possível, e o próprio compartilhamento dificulta mudanças
Se tudo estiver em uma única função, a maioria dos desenvolvedores vai tentar mudar essa função para atender aos 10 casos — mesmo que ela nunca devesse ter sido uma função única para começo de conversa
Em vez de desfazer um nó mal dado que já amarrou várias partes do sistema, é muito mais fácil corrigir os dez lugares copiados e colados
À primeira vista, isso parece uma contradição com o próprio tema, mas, lendo com calma, ele usa copiar e colar como um sinal para mostrar qual código deveria ser abstraído e quais padrões realmente devem ser seguidos
É estranho continuar repetindo todos esses mandamentos sobre software, esses princípios quase religiosos
No papel, todos parecem ótimos e fazem sentido, mas, mesmo depois de 50 anos, o software continua sendo lixo em 90% dos casos
E ainda assim essas coisas seguem sendo apresentadas como grandes insights ou balas de prata
Há um excelente corolário aqui. Código ruim é muito mais difícil de remover, então permanece por mais tempo