4 pontos por GN⁺ 2024-10-30 | 1 comentários | Compartilhar no WhatsApp
  • 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 util como um diretório, não como um único arquivo, e colocar utilitários diferentes em arquivos diferentes
    • Um único arquivo util tende a crescer sem parar e, quando fica grande demais, se torna difícil de dividir
  • 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 verboso urllib3
    • requests lida com workflows comuns de uso de HTTP e esconde detalhes práticos
    • urllib3 oferece 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

 
GN⁺ 2024-10-30
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_...

    • Verdade, mas para aplicar o princípio de simplicidade é robustez também é preciso entender a complexidade inerente
      O código não fica robusto só por ignorar casos de borda, mesmo que pareça mais simples por causa disso
    • A regra que sigo é esta: na primeira vez, só escreva; na segunda, copie; na terceira, considere refatorar
    • Concordo, mas não sei se a expressão simple is robust é intuitiva o bastante
      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
    • Concordo fortemente. Lixo demais em software nasce de tentar resolver problemas imaginários
      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ê?
    • A complexidade da lógica de negócio não desaparece só porque você quer. Se ela é vasta e interligada, o código também será
  • 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

    • Você pode até tornar algo fácil de remover, mas outras pessoas podem muito bem criar abstrações e lógica, enfiar isso no núcleo do projeto e depois endurecer tudo a ponto de ficar impossível de eliminar mais tarde
      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
    • Ainda assim, depende do contexto. Para aplicações de negócios, isso está certo, certíssimo
      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
    • Algumas coisas mudam, e às vezes você escolhe a abstração errada
      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ção
    Entã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

    • Simplificando um pouco, se você cria algo fácil de remover, acaba evitando bugs não intencionais ao remover
      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
    • Dá para esperar que, se há muitas linhas de código, também haja em certa medida muitas linhas de testes
      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
    • Testes são bons, mas programação não se resume a escrever testes. Nem todo texto precisa mencionar 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

    • Pela minha experiência, código ruim de copiar e colar resulta em uma tarde irritante de pagamento de dívida técnica e correções
      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
    • Muitas vezes, código reutilizado é código correto em vários lugares, então, para mudá-lo, você precisa desacelerar e separar esses pontos
      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
    • O que acontece se, de 10 chamadas de função, 3 precisarem mudar de um jeito, 5 de outro, e as 2 restantes nem usarem mais a mesma abstração e precisarem ser completamente reescritas?
      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
    • Se eu fosse o autor, provavelmente responderia que aquele código deveria ter sido movido para um módulo ou função
      À 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

    • Acho que esses 90% de lixo são escritos por pessoas que não leem nem escrevem textos como este
  • Há um excelente corolário aqui. Código ruim é muito mais difícil de remover, então permanece por mais tempo