35 pontos por GN⁺ 2024-12-23 | 4 comentários | Compartilhar no WhatsApp
  • O software moderno é atualizado com frequência por meio de entrega contínua (CD) e testes automatizados (CI), mas o "software usado no longo prazo" exige uma abordagem diferente
    • Exemplos: usinas nucleares, aviões, marcapassos, sistemas eleitorais etc.
      • Em áreas nas quais confiabilidade e estabilidade são cruciais, prefere-se estabilidade e mudanças previsíveis em vez de mudanças contínuas

Princípios centrais do desenvolvimento de software de longo prazo

Dependências (Dependencies)

  • As dependências de software são um fator importante para o sucesso no longo prazo
  • O software precisa considerar sua interação com o mundo externo, e escolhas fundamentais como a linguagem de programação são importantes
  • Entender a hierarquia das dependências de software
    • Mundo externo: software cliente que não controlamos (por exemplo, navegadores).
    • Escolhas fundamentais: elementos que só podem ser alterados reescrevendo toda a stack, como a linguagem de programação.
    • Frameworks: Spring Framework, React etc., fortemente acoplados à base de código. É possível trocá-los, mas com custo muito alto.
    • Banco de dados: na maioria dos casos pode ser substituído, mas exige ajustes finos e trabalho.
    • Bibliotecas auxiliares: bibliotecas substituíveis que fornecem funcionalidades específicas.
  • Com o tempo, as dependências e o mundo externo mudam:
    • Mudanças nas dependências podem causar modificações no código ou mudanças de comportamento.
    • O lançamento de novas versões principais pode gerar problemas de compatibilidade.
    • Há risco de o projeto ser descontinuado ou desaparecer.
    • Riscos de segurança: a dependência pode ser comprometida por agentes maliciosos (npm, PyPI etc.).
    • Comercialização: um novo proprietário, como um fundo de venture capital (VC), pode transformá-la em produto pago.
    • Problemas de conflito entre dependências.
  • Itens a verificar ao escolher dependências pensando no uso de longo prazo:
    • Nível técnico: é possível avaliar a qualidade olhando o código-fonte?
    • Base de usuários: verificar quem já usa.
    • Objetivo de desenvolvimento: entender quem desenvolve e quais são suas metas.
    • Financiamento: se há apoio financeiro e qual sua origem.
    • Manutenção: verificar se há lançamentos de segurança com regularidade.
      • Se a comunidade pode assumir a manutenção.
      • Se eu mesmo posso manter.
      • Se, quando necessário, devo apoiar financeiramente para garantir a sustentabilidade do projeto.
    • Dependências das dependências:
      • Revisar também o histórico de segurança das dependências transitivas.
  • Abordagem realista
    • Limitar dependências:
      • Um projeto com mais de 1600 dependências tem grande chance de mudar rapidamente e se tornar instável.
      • Em projetos com inúmeras dependências, fica difícil até mesmo saber que código está sendo distribuído.
    • Adicionar com cautela:
      • Ao adicionar uma dependência, atribua a ela um peso técnico para criar naturalmente um tempo de revisão.
      • Em projetos de longo prazo, dependências desnecessárias devem ser evitadas.

Dependências de runtime (Runtime Dependencies)

  • O que foi discutido até aqui se limita a dependências de build/compilação.
  • Porém, projetos modernos frequentemente incluem também dependências de runtime:
    • Exemplos: Amazon S3, Google Firebase.
    • Algumas são tratadas quase como padrão de fato (como o S3).
    • Mas a maioria das dependências de runtime tem forte característica de lock-in a um serviço específico.
  • Daqui a 10 anos, encontrar uma alternativa que substitua o serviço usado hoje pode gerar um custo muito alto.
  • É preciso minimizar ou zerar a lista de dependências de serviços de terceiros:
    • Especialmente no desenvolvimento de software cloud native, é comum usar muitos serviços avançados de terceiros.
    • Em projetos de longo prazo, esse tipo de dependência traz alto risco.
  • Dependências de serviços em build time também são importantes:
    • Exemplo: se npm install deixar de funcionar, o próprio build do software pode se tornar impossível.
    • Isso pode reduzir severamente a reutilização do projeto.
  • Revise cuidadosamente as dependências de runtime:
    • Reconheça problemas potenciais de lock-in e reduza ou elimine dependências.
  • Garanta a viabilidade de manutenção no longo prazo:
    • Considere com antecedência a possibilidade de substituir serviços de nuvem ou de terceiros.

Testes, testes e mais testes

  • A necessidade de testes é um princípio básico com o qual todos concordam:
    • Escreva o máximo de testes possível.
    • Nem todos os testes têm o mesmo valor, mas quase nunca se arrepende de ter testes.
  • Especialmente em projetos com muitas dependências, testes são essenciais:
    • Se dependências mudarem ou sofrerem drift, eles ajudam a detectar problemas cedo.
  • O papel dos testes
    • Ajudar a resolver problemas:
      • Permitem ajustar rapidamente o sistema conforme as mudanças.
    • Apoiar refatorações:
      • Dão confiança ao remover ou alterar dependências do código.
    • Úteis para manutenção de longo prazo:
      • Mesmo depois de mais de 3 anos sem desenvolvimento, os testes permitem verificar se o sistema ainda funciona.
      • Também permitem confirmar se a funcionalidade continua preservada em novos compiladores, runtimes e sistemas operacionais.
  • Testes não são custo, são investimento
    • Escreva mais testes:
      • Testes são a base da manutenção e da estabilidade.
      • Ao modificar ou expandir código, eles oferecem um grande apoio mental.

Complexidade: o chefão final do desenvolvimento de software

  • A complexidade é o inimigo supremo do desenvolvimento de software:
    • Até os melhores desenvolvedores ou equipes podem ser derrotados pela complexidade.
    • Sob a influência da entropia e do comportamento humano, a complexidade sempre aumenta.
    • Se ela não for gerenciada conscientemente, o projeto pode cair em um estado impossível de manter.
  • A correlação entre complexidade e volume de código
    • Quantidade de código e complexidade:
      • Quando há pouco código, mesmo algo relativamente complexo ainda pode ser gerenciável.
      • À medida que o código cresce, é preciso manter a simplicidade para continuar no controle.
      • A complexidade gerenciável precisa ficar dentro da capacidade da equipe e dentro do "triângulo verde".
    • Limites da complexidade:
      • Mesmo aumentando a equipe ou contratando desenvolvedores excepcionais, há um limite para lidar com complexidade.
      • Ao ultrapassar esse limite, o projeto entra em estado de impossibilidade de manutenção.
  • Por que o código sempre se move para “cima e à direita” (no gráfico):
    • Mais pedidos de funcionalidades.
    • Tentativas de otimização desnecessárias.
    • Ao corrigir bugs, adiciona-se código novo em vez de reduzir a complexidade existente.
  • O custo de um design de API ruim:
    • Exemplo: a função CreateFile não cria um arquivo na maioria dos casos.
    • Esse tipo de confusão aumenta a carga cognitiva adicional e a chance de erro.
  • Estratégias para gerenciar complexidade
    • Refatore cedo e com frequência:
      • Remova código desnecessário e invista tempo em simplificação.
    • Invista em testes:
      • Quanto mais testes houver, mais fácil fica reduzir a complexidade.
    • A importância de gerenciar a complexidade:
      • Se não houver esforço antecipado para simplificar, projetos de longo prazo correm o risco de acabar em um estado "impossível de manter".

Escreva código chato e simples. Mais simples ainda. E mais chato ainda.

"Depurar é duas vezes mais difícil do que escrever um programa. Portanto, se você escreve o código o mais esperto possível, como vai depurá-lo?" - Brian Kernighan

  • Escreva código super chato e claro:
    • Prefira código ingênuo (naive), mas intuitivamente compreensível.
    • "A otimização prematura é a raiz de todo mal."
  • Otimize só quando realmente for necessário:
    • Se algo for simples demais a ponto de causar problema, não será difícil adicionar complexidade depois.
    • Esse momento pode nunca chegar.
  • Evite escrever código complexo:
    • Espere até o momento em que isso for realmente necessário.
    • É muito improvável que você se arrependa de ter escrito código simples.
  • Código de alto desempenho ou funcionalidades avançadas podem funcionar apenas em ambientes específicos.
    • Exemplos:
      • LMDB: o PowerDNS passou por muitas dificuldades até usá-lo de forma estável.
      • RapidJSON: biblioteca JSON com aceleração SIMD. Tem ótimo desempenho, mas condições de uso exigentes.
  • Mesmo que você tenha confiança de que "consigo superar essa limitação":
    • Talvez isso funcione este ano, mas daqui a 5 anos você ou o próximo desenvolvedor podem sofrer.
    • O mesmo princípio se aplica a linguagens de programação complexas.
  • Conclusão:
    • Simplifique o código:
      • Deixe realmente simples. Mais simples ainda.
    • Deixe a otimização para depois:
      • A complexidade pode ser adicionada quando necessário, mas se ela for introduzida cedo, a manutenção fica difícil.

Desenvolvimento de software baseado em LinkedIn

  • Realidade vs. ideal
    • Abordagem ideal: ao escolher dependências, é preciso avaliar e revisar cuidadosamente (usando o checklist apresentado acima).
    • Abordagem realista: às vezes há a tendência de experimentar uma tecnologia atraente e, se funcionar, continuar usando.
  • Por que isso é atraente
    • Tecnologias recomendadas por figuras famosas ou influenciadores do LinkedIn.
    • O "framework mais novo" altamente elogiado em comunidades como o Hacker News.
  • Tecnologias da moda não têm validação de longo prazo suficiente:
    • Podem não ser adequadas para projetos de software que precisam durar mais de 10 anos.
    • Tecnologias novas têm maior chance de apresentar problemas de estabilidade e manutenção nos estágios iniciais.
  • Recomendações
    • Use apenas em áreas experimentais:
      • Teste tecnologias novas primeiro em projetos pequenos ou em áreas não essenciais.
    • Considere o efeito Lindy:
      • A vida útil de uma tecnologia tende a ser proporcional ao tempo que ela já está em uso.
      • Quanto mais antiga a tecnologia, maior a expectativa de estabilidade de longo prazo.
  • Tecnologias novas são atraentes, mas para projetos de longo prazo, tecnologias comprovadas e estáveis são mais adequadas.

Logs, telemetria e desempenho

  • Se o software não for continuamente atualizado ou implantado:
    • É grande a chance de não haver feedback imediato quando um site quebra.
    • Pode levar muito tempo entre o deploy e a solução do problema real.
  • Implemente logs e telemetria de forma rigorosa desde a primeira versão:
    • Registre desempenho, falhas e atividade do software.
    • Com o tempo, os dados acumulados se tornam muito úteis para resolver bugs raros.
  • Problemas causados por logs insuficientes:
    • Uma UI foi implantada e um usuário que criou 3000 pastas relatou um problema.
    • O usuário apenas disse "não funciona", e levou meses para descobrir a causa raiz.
    • Se houvesse logs de desempenho e telemetria, o problema poderia ter sido resolvido muito mais rapidamente.
  • Logs e telemetria são essenciais:
    • O software deve ser projetado para permitir monitoramento rigoroso de sua atividade.
    • Isso ajuda muito a resolver problemas inesperados durante implantações e manutenção de longo prazo.

Documentação

  • A importância da documentação:
    • Não basta apenas escrever boa documentação de API; é preciso explicar "por que foi projetado assim".
    • Registre as ideias e a filosofia de como o sistema funciona.
    • É preciso deixar registrado por que as soluções foram separadas e a justificativa de decisões de design não intuitivas.
  • Materiais úteis além da documentação de arquitetura:
    • Posts internos de blog: desenvolvedores compartilham discussões mais livres sobre o design do sistema.
    • Entrevistas com a equipe: registros de conversas sobre o contexto das decisões de design.
    • Esses documentos permitem a transferência de conhecimento dentro da equipe mesmo com o passar do tempo.
  • Deixe comentários no código:
    • Apesar da tendência de dizer que "bom código não precisa de comentários", comentários que explicam o 'porquê' do código são essenciais.
    • É importante explicar por que uma determinada função existe.
  • Escreva mensagens de commit:
    • As mensagens de commit são o núcleo do histórico de trabalho. Elas ajudam a rastrear o motivo das mudanças no código.
    • Crie um ambiente em que as pessoas possam consultar facilmente essas mensagens.
  • Reserve tempo para documentar:
    • Em dias em que o desenvolvimento não rende bem, dedique tempo a deixar comentários e registros úteis.
    • Em nível de equipe, reserve regularmente tempo para documentação.
  • Registre por que o design foi feito dessa forma:
    • Daqui a 7 anos, materiais que transmitam a filosofia e o contexto a uma nova equipe serão mais valiosos do que qualquer outra coisa.
  • Deixe a história registrada por meio de comentários e mensagens de commit:
    • Isso é essencial não apenas durante o desenvolvimento, mas também para a manutenção de longo prazo.

Formação da equipe

  • A continuidade da equipe e o sucesso de longo prazo do software:
    • Alguns softwares são projetados para ter suporte por 80 anos. Em projetos tão longos, manter a equipe é fundamental.
    • No ambiente moderno de desenvolvimento, uma permanência média de cerca de 3 anos já é considerada longa.
    • Boa documentação e testes podem compensar parcialmente a troca de equipe, mas há limites.
  • Vantagens da permanência de longo prazo:
    • Manter membros da equipe por mais de 10 anos:
      • É importante contratá-los como funcionários de fato e gerenciar bem os desenvolvedores.
      • Isso é considerado um "hack" essencial para o sucesso de projetos de longo prazo.
  • Problemas de depender de terceirização:
    • Desenvolvedores terceirizados muitas vezes entregam o código ao sistema e vão embora.
    • Se o objetivo é uma qualidade de software sustentável por mais de 10 anos, essa é uma forma muito ineficiente de trabalhar.
  • Crie um ambiente em que os membros da equipe possam permanecer juntos no longo prazo.
  • É necessário adotar estratégias para minimizar a dependência de consultores externos e aumentar a sustentabilidade da equipe interna.

Considere open source

  • Vantagens do open source:
    • Manter a qualidade do código por meio de revisão externa:
      • Olhares externos exigem padrões mais altos dos desenvolvedores.
    • É um mecanismo poderoso para manter padrões de código melhores.
  • A realidade da preparação para abrir o código:
    • Empresas ou governos frequentemente alegam que levaria de meses a anos para preparar algo como open source.
    • Motivos:
      • Internamente, é comum existir código de que se tem vergonha de expor externamente.
      • Porque é necessário limpar o código antes de abri-lo.
  • Avalie a aplicabilidade:
    • Open source nem sempre é uma opção possível.
    • Quando for possível, é uma boa forma de elevar a qualidade do código e a transparência.
  • Open source é uma estratégia importante a ser usada quando viável.
  • O olhar externo e padrões mais altos ajudam a manter o projeto na direção certa.

Verificação da saúde das dependências

  • O problema das mudanças nas dependências:
    • As dependências podem mudar ou se desviar do esperado com o tempo.
    • Se isso for ignorado, pode levar a:
      • bugs
      • falhas de build
      • outros resultados frustrantes.
  • Recomenda-se uma verificação regular de saúde:
    • Revisões periódicas de dependências:
      • Oferecem a chance de descobrir problemas com antecedência.
      • Também permitem encontrar novos recursos nas dependências e explorar a possibilidade de simplificar o código ou remover outras dependências.
    • A importância da manutenção preventiva:
      • Se você não planejar tempo para revisar por conta própria, acabará sendo forçado a gastar esse tempo quando o problema aparecer.
  • Uma metáfora de manutenção:
    • Um ditado dos mecânicos:
      • "Planeje você mesmo o tempo de manutenção. Caso contrário, o equipamento vai planejar esse tempo por você."
  • Revisões regulares de dependências são uma atividade essencial para a estabilidade e a eficiência do software no longo prazo.
  • Use isso como oportunidade para resolver problemas com antecedência e descobrir mudanças positivas.

Principais livros de referência

Por fim

Principais recomendações para o desenvolvimento de software de longo prazo:

  • Mantenha a simplicidade:
    • Simples, e mais simples ainda! Como a complexidade pode ser adicionada quando necessário, não torne as coisas excessivamente complexas logo no início.
    • Para manter a simplicidade, são necessárias refatorações periódicas e remoção de código.
  • Pense cuidadosamente sobre dependências:
    • Quanto menos dependências, melhor. Revise e audite com cuidado.
    • Se você não consegue auditar 1600 dependências, precisa repensar o plano.
    • Evite escolhas guiadas por tendências ou modismos (por exemplo, desenvolvimento baseado em LinkedIn).
    • Revisões regulares de dependências: monitore continuamente o estado das dependências.
  • Testes, testes e mais testes:
    • Detecte dependências em mudança no momento certo.
    • Oferecem confiança durante refatorações e ajudam a manter a simplicidade.
  • Documentação:
    • Documente não apenas o código, mas também a filosofia, as ideias e o contexto de "por que isso foi feito assim".
    • Isso se torna um ativo valioso para futuros membros da equipe.
  • Mantenha uma equipe estável:
    • Considere contratações de longo prazo como investimento em projetos duradouros.
    • Dê suporte para que os membros da equipe possam se dedicar ao projeto por longos períodos.
  • Considere open source:
    • Quando possível, use open source para manter padrões de código mais altos.
  • Logs e telemetria de desempenho:
    • Têm papel importante para identificar e resolver problemas cedo.
  • Essas recomendações podem não ser novas, mas como são enfatizadas por desenvolvedores experientes, vale a pena refletir profundamente sobre elas.

4 comentários

 
kandk 2024-12-30

A principal capacidade de engenharia é separar as camadas em que a estabilidade é importante das camadas em que a velocidade é importante e decidir como lidar com a relação entre elas.
Se a Toss buscasse apenas estabilidade, não seria diferente dos outros bancos.

 
kandk 2024-12-30

O perigoso é que a SpaceX também é assim. A Tesla também...

 
aer0700 2024-12-25

Será que desenvolvimento guiado por currículo é o problema.

 
GN⁺ 2024-12-23
Comentários no Hacker News
  • Atualizar ativamente a toolchain é uma parte importante do processo de desenvolvimento. Muitas empresas tiram os upgrades da toolchain da lista de prioridades, o que acaba causando problemas como vulnerabilidades de segurança. A cada novo release do compilador ou do sistema de build, cria-se uma branch para verificar o estado da build e, se houver erro, isso é tratado como bug e corrigido imediatamente. Isso ajuda a modernizar e refatorar gradualmente a base de código com recursos mais novos da linguagem.

  • Dependências de terceiros costumam ser decepcionantes no longo prazo. Em projetos novos, elas podem resolver problemas no curto prazo, mas, no longo prazo, é melhor substituí-las por código próprio.

  • É necessário fazer vendoring das dependências e gerenciá-las por meio de code review. Muitas vezes, a qualidade do código de terceiros é baixa, e escrever diretamente pode ser uma opção melhor.

  • Há um projeto em andamento usando Qt, CMake e C++ moderno com foco em escalabilidade de longo prazo. Essa stack tecnológica continua oferecendo recursos e melhorias.

  • Trabalhar com Emacs Lisp foi uma experiência revigorante. Uma vantagem é que ele continua funcionando de forma estável mesmo quando as bibliotecas não são atualizadas. Já a experiência com Gatsby e Node foi difícil por causa dos problemas com atualizações.

  • Escrever código simples é importante. Código complexo deve ser escrito apenas quando necessário, e código simples não gera arrependimento.

  • A documentação de sistemas e código é importante. Quanto mais experiência se tem em desenvolvimento de software, mais se percebe a importância da documentação.

  • Testes têm um papel importante no planejamento. Vale tomar como referência a forma de desenvolvimento da NASA, concentrando esforços em encontrar erros de programação. No desenvolvimento de software médico, evita-se interpretação e não se usa alocação dinâmica de memória.

  • A melhor forma de escrever software duradouro é escrever código "entediante". É preciso evitar dependências e manter-se fiel ao básico.

  • Houve experiência com dificuldades causadas por dependências em Python. Isso é chamado de "DLL Hell", e o COM tentou resolver esse problema, mas não teve sucesso.

  • As práticas aplicadas a software industrial não são robustas o suficiente para serem aplicadas ao software em geral. Os engenheiros tentam mitigar riscos, e nós nos concentramos em mitigar riscos.