Por que não tem problema não entender completamente a base de código
(seangoedecke.com)- Em sistemas de grande porte com dezenas de milhões de linhas, ninguém consegue manter o todo na cabeça, então os engenheiros precisam conseguir trabalhar de forma eficaz mesmo com um entendimento parcialmente correto
- Programming as Theory Building, de Peter Naur, defende que, quando o entendimento de uma equipe existente se perde, é melhor descartar o programa e criar outro do zero, mas sistemas grandes, com usuários e incontáveis exceções entrelaçadas, são difíceis de reconstruir desde o início
- Mesmo uma base de código cujo responsável já saiu pode ser recuperada se você entender um fluxo de ponta a ponta e depois ampliar com cuidado o escopo das mudanças; em grandes organizações, essa reconstrução do entendimento acontece repetidamente
- LLMs ao mesmo tempo atrapalham a formação de modelos mentais detalhados e ajudam a criar e usar rapidamente entendimentos parciais; colaboração, exigências legais, atualizações de segurança e adoção de dependências também exigem trocas entre entendimento do código e outros valores
- Um modelo mental preciso ajuda em um desenvolvimento mais agradável e estável, mas não é um critério absoluto; no trabalho real, às vezes é preciso abrir mão do entendimento completo em nome de velocidade, conformidade legal e exigências organizacionais
Entendimento completo e entendimento parcial
- Em bases de código pequenas e com pouca troca de integrantes, é fácil pensar que só dá para fazer um bom trabalho entendendo tudo
- Projetos como Redis ou The Witness) se encaixam nisso
- Em bases de código como o backend de busca da web do Google ou o GitHub, que são grandes e têm alta rotatividade de pessoas, não é possível entender tudo; por isso, cada um trabalha entendendo o máximo possível de sua área local
- Os dois ambientes diferem bastante em método de desenvolvimento, práticas e cultura, mas nas discussões online sobre engenharia de software a primeira cultura, que valoriza o entendimento completo, é representada em excesso
- Engenheiros de open source têm mais incentivo para compartilhar seu trabalho por escrito, e o trabalho de engenharia pura tende a se destacar mais do que em sistemas proprietários de grande escala
- Sistemas proprietários são difíceis de divulgar por razões legais e, mesmo quando isso é possível, explicar uma base de código grande exige contexto específico demais
- Em muitos ambientes de software, o entendimento parcial não é um estado incorreto; em sistemas grandes, ele é realisticamente o melhor que se pode alcançar
- Essa diferença leva ao conflito entre a cultura do entendimento completo e a cultura do entendimento parcial, distinção apresentada em Pure and impure software engineering
O argumento de Programming as Theory Building
- O artigo Programming as Theory Building, de Peter Naur, entende que o principal produto criado pelo programador não é o código, mas uma teoria sobre o programa
- Essa teoria é composta por um entendimento intuitivo do que acontece e por quê
- Código e documentação só conseguem conter esse entendimento de forma parcial
- Mesmo que o código se perca, uma equipe que detém essa teoria consegue reescrever o programa; mas, se a equipe for totalmente substituída e o entendimento se perder, fica difícil compreender o código existente
- Segundo Naur, não é possível reconstruir a teoria existente apenas com documentação ou código; por isso, o programa original deveria ser descartado e uma nova equipe deveria resolver o problema novamente, do zero
- The Concept of Mind, de Gilbert Ryle, citado por Naur, enxerga de forma mais ampla o escopo dessa construção de teoria
- A teoria ou o know-how podem surgir naturalmente enquanto se realiza algo na prática
- Portanto, o processo de explorar o próprio código para entender uma base de código existente também é compatível com essa abordagem
Por que não dá para reescrever sistemas grandes do zero
- Um sistema suficientemente grande e com usuários acumula milhares de casos excepcionais e comportamentos peculiares que são difíceis de reimplementar
- Mesmo uma equipe que conhece bem o sistema não consegue considerar todos os detalhes ao mesmo tempo, então é difícil reescrevê-lo inteiro de uma vez
- Reescritas bem-sucedidas avançam dividindo a base de código existente em partes pequenas e isoladas, para então substituir uma parte por vez
- No fim, até uma reescrita é um trabalho de aplicar uma série de mudanças ao sistema existente
- Se não for possível alterar o sistema atual, substituí-lo completamente por um sistema novo é ainda mais difícil
Como recuperar uma base de código abandonada
- Em empresas de tecnologia com centenas de milhões de linhas de código e milhares de engenheiros, não é raro que não reste ninguém que conheça uma base de código específica
- Basta que algumas pessoas responsáveis saiam em um momento ruim, ou que a base fique um ano sem manutenção, para chegar a esse estado
- Naur considerava que dificilmente a recuperação de um programa totalmente morto seria entregue a novos programadores sem ninguém que conhecesse minimamente a teoria existente, mas isso de fato acontece em grandes organizações
- Mesmo uma base de código abandonada pode voltar a um estado em que seja possível construir um novo entendimento e trabalhar nela de forma eficaz, desde que se invista tempo
- Primeiro, entende-se um fluxo de processamento do começo ao fim
- Depois, com mudanças cuidadosas, esse entendimento é expandido desse fluxo para áreas ao redor
Todo mundo trabalha com teorias imperfeitas
- O software moderno de grande escala é tão grande que nem indivíduos nem equipes inteiras conseguem manter todos os seus comportamentos na cabeça
- Em uma base de código grande o bastante, todos acabam trabalhando com uma teoria imprecisa sobre o programa em algum grau
- Um engenheiro eficaz não espera até que alguém com entendimento perfeito apareça com a resposta; ele toma a decisão mais bem fundamentada possível com as informações atuais e depois lida com o resultado
- Esse tipo de trabalho exige capacidade de tomar posição e confiança mesmo em situações incertas
- A realidade de que ninguém entende completamente o comportamento total de um produto grande é a mesma discutida em Nobody knows how software products work
A era de Naur e o tamanho do código hoje
- Quando Naur escreveu o artigo, em 1985, o tamanho médio dos programas provavelmente era várias ordens de grandeza menor do que hoje
- O primeiro exemplo de programa grande dado por Naur era um sistema de monitoramento industrial com 200 mil linhas, e o segundo era um compilador
- A primeira versão do GCC tinha cerca de 100 mil linhas em 1987, mas em 2015 passou de 14 milhões de linhas
- Se os testes existentes puderem ser reaproveitados, programas de 100 mil a 200 mil linhas podem ser reescritos com relativa facilidade, mas é difícil aplicar o mesmo julgamento a sistemas com 1 a 2 milhões de linhas ou mais
A ambivalência dos LLMs na construção de teoria
- LLMs muitas vezes são vistos como ferramentas ruins por atrapalharem o processo geral de construção de teoria
- Mas, como qualquer outra ferramenta de software, os LLMs também têm dois lados
- Pode ficar mais difícil construir um modelo mental detalhado sobre o software
- Teorias parciais podem ser construídas rapidamente
- Eles podem ajudar a trabalhar de forma mais eficaz mesmo com base em entendimento incompleto
- A relação entre LLMs e entendimento de código não pode ser resumida como uma simples lista de vantagens e desvantagens; ela continua sendo um trade-off complexo que exige julgamento
Escolhas necessárias, mesmo que dificultem o entendimento do código
- Além dos LLMs, há muitos outros fatores que dificultam manter uma teoria precisa da base de código
- Permitir que outras pessoas escrevam código na mesma base
- Implementar funcionalidades exigidas por lei, como acessibilidade ou proteção de dados
- Permitir que colegas saiam da empresa ou mudem de equipe
- Atualizar versões de software por causa de patches de segurança
- Introduzir bibliotecas ou outras dependências
- Não dá para concluir que uma ferramenta ou prática é ruim só porque atrapalha a construção de teoria
- Entender a base de código é apenas um entre vários valores de engenharia, assim como legibilidade, manutenibilidade e correção
- Dependendo da situação, pode fazer sentido priorizar o entendimento preciso e sacrificar outros valores
- Por outro lado, também se pode abrir mão do entendimento do código em nome de velocidade, conformidade legal ou razões políticas dentro da organização
- O contra-argumento de que o entendimento do código é a base para realizar todos os outros valores vale igualmente para legibilidade, manutenibilidade e correção; na prática, até esses valores centrais são constantemente negociados
Preferência pessoal e responsabilidade profissional
- Especialmente para engenheiros puros, trabalhar mantendo um modelo mental preciso é o preferido
- O desenvolvimento fica mais divertido e menos estressante
- Parece mais próximo daquilo que a própria pessoa considera engenharia de verdade
- Muitos engenheiros também criam pequenos projetos open source sozinhos no tempo livre justamente porque isso lhes permite trabalhar mantendo uma teoria no estilo de Naur precisa sobre a base de código
- No trabalho, porém, é preciso seguir o conjunto de valores que a organização está pagando para priorizar, e não apenas os valores de engenharia pessoais
- Mesmo que desempenho seja importante, às vezes é preciso escrever código lento para cumprir prazos ou acomodar requisitos difíceis
- O entendimento completo da base de código também não é um padrão absoluto que sempre deva ser preservado, mas uma opção que pode ser trocada por outros valores conforme o objetivo do trabalho
2 comentários
Por isso ficamos tão obcecados por abstração.
Opiniões no Lobste.rs
Infelizmente, o título do texto original beira o caça-cliques; o conteúdo real está mais para ser capaz de avançar mesmo com um entendimento parcial de um codebase grande
Mesmo sob a ótica de construção de teoria, se considerarmos que uma teoria tem tanto largura quanto profundidade, isso não entra em conflito com o ponto central do texto. Largura significa até que ponto do sistema você entende bem o bastante para responder perguntas e fazer mudanças com habilidade, enquanto profundidade significa a que nível de perguntas complexas sobre uma parte específica você consegue responder e fazer mudanças complexas preservando a integridade
Se você reescreve o código por módulos e mantém um período de transição, então está construindo uma teoria estreita, porém profunda, sobre aquele módulo e trabalhando com base nela. Em funções pequenas, pode ser aceitável adicionar um parâmetro ou valor de retorno, mas em escalas maiores faz mais sentido ao menos recomeçar do zero com um módulo específico em estado limpo e conectar o restante para chamar a nova implementação. No fundo, tentar melhorar gradualmente a correção de uma implementação fundamentalmente cheia de bugs não costuma funcionar bem, e isso também se aplica com frequência a propriedades como desempenho
Ainda assim, quando Naur disse que “em vez de reviver um programa, deve-se descartar o texto do programa existente e fazer uma nova equipe de programadores resolver o problema dado desde o início”, há espaço para discutir se isso quer dizer apagar primeiro o código existente antes de começar a reescrita, ou se ele pode ser descartado só depois de confirmar que o novo programa é um substituto adequado
Além disso, no mundo real esse problema dado em si muitas vezes nem é dado. Naur pressupõe que a definição original do problema ainda existe, mas isso muitas vezes não acontece em codebases grandes. O mais próximo geralmente é a suíte de testes, então é possível substituir componentes continuando a passar nos testes
Naquela época também não existia entrega contínua. Hoje, é preciso manter o serviço rodando, atender chamados, responder perguntas e corrigir bugs urgentes enquanto tudo segue em execução, então a conta de custo-benefício de uma reescrita muda
Há partes bem formuladas nessa objeção, mas a maior parte parece óbvia. Em sistemas suficientemente grandes, o entendimento parcial é o único estado possível, mas a organização ainda deve sempre buscar um entendimento mais profundo
Independentemente da lógica de “somos pagos para fazer o trabalho”, softwares grandes com frequência falham em satisfazer usuários ou atingir metas de negócio. Também dá para dizer que Windows e OS X estão apodrecendo sob o peso da complexidade acumulada
Se a gerência pudesse fazer a complexidade desaparecer por magia, faria isso. Só que esse ainda é um problema não resolvido da engenharia de software, e não tanto um caso de a gerência querer lançar código ruim, mas sim de aceitar esse tipo de compromisso nas condições atuais
Dá para argumentar que “se houver monopólio, basta isso, então dá para lançar software ruim e cheio de bugs”, mas isso é niilista demais. Há muitas empresas de software que não são monopólios ou que não conseguem prender os usuários, então nesses casos a qualidade realmente importa
Sob essa interpretação, a própria noção de entendimento também parece funcionar como uma espécie de controle de acesso
A abordagem descrita no texto está intimamente relacionada à capacidade de raciocinar localmente sobre partes do código
O raciocínio local — entender uma parte de um codebase grande sem compreender tudo ao redor — é um conceito central que a ciência da computação persegue desde o começo
A principal vantagem da programação estruturada era justamente permitir raciocínio local, e a recomendação de evitar variáveis globais também surgiu em grande parte com esse objetivo. A programação funcional permite raciocínio local ao eliminar efeitos colaterais, e um dos princípios da programação orientada a objetos — combinar a estrutura de dados com o código que a manipula — também fornece outro meio de raciocinar localmente
A verdadeira força desses conceitos básicos está em permitir, ainda que não seja a situação ideal, raciocinar e trabalhar sobre pequenas partes sem entender o codebase inteiro
Li isso como uma contraposição interessante ao conselho de que “é preciso entender cada linha do codebase”, e como a tese central do texto toca em práticas reais de desenvolvimento, marquei com a tag
#practicesEu sei que esta comunidade não gosta de LLMs, mas ainda assim fico em dúvida se este texto realmente se enquadra em
#vibecodingvibecoding; eu também não concordo, mas essa é a prática. A pergunta central deste texto também está intimamente ligada à programação assistida por LLMAinda assim, concordo que
vibecodingnão deveria ser a única tag. O texto só passa a tratar explicitamente de LLM no final, e o argumento central já era interessante e digno de discussão antes mesmo da era dos LLMs. Por isso, vejo base suficiente para restaurar a tagpracticese a restaurei eu mesmo, recomendando que outros façam o mesmoEle também continua relevante em um mundo onde o uso de LLM pode atrapalhar uma compreensão completa do codebase
Se a tag significar “pode ser relevante para quem se interessa por vibe coding”, então ela claramente faz sentido. Se, ao contrário, significar “não tem relação para quem não se interessa por vibe coding”, então não faz sentido. Todas as outras tags são usadas no primeiro sentido, mas uma parcela considerável dos usuários do Lobsters usa só esta tag no segundo sentido, então eles acabam inevitavelmente deixando passar este texto
Outra característica do desenvolvimento moderno, diferente de 1985, é que mesmo se você se tornar especialista em um codebase de 300 mil linhas, na semana seguinte pode estar trabalhando em outro codebase de 300 mil linhas completamente diferente
Programadores experientes de mercado estão acostumados a poder ser jogados em território desconhecido a qualquer momento e contam com uma combinação de expressões idiomáticas comuns (Google C++ Style Guide), ferramentas automatizadas como compiladores que geram erro quando a API é usada incorretamente, e uma intuição sobre “como programas grandes devem ser organizados”
Também já vi discussões do tipo “agora que ninguém entende o comportamento do codebase inteiro, como será possível continuar desenvolvendo?”, como no caso do Bun reescrito com LLM, mas isso é parecido com perguntar como alguém pode entender um romance sem ser o autor. No fim, é código, então basta ler. Se alguma função não fizer sentido, você pode quebrá-la em várias partes, escrever testes, anotar o fluxo de controle no papel ou tentar qualquer outra coisa
Como você acaba mudando de projeto com regularidade, também não há motivo para continuar mantendo um entendimento de codebases antigos. Em algum momento tudo começa a se misturar de forma parecida. Mesmo que você tenha de ajudar o código Scala de uma startup adquirida pela empresa a se comunicar, via um protocolo RPC customizado baseado em Thrift, com um serviço Ruby baseado em JSON que está sendo reescrito em Go, uma hora de busca por
Scala syntax referenceeThrift wire encodingjá basta para começarNem é preciso virar especialista. Afinal, um mês depois você pode estar depurando por que um verificador de tipos baseado em OCaml para JavaScript está travando na implementação em Go do kernel Linux. No fim das contas, é tudo código.