Desenvolvimento de software de longo prazo
(berthub.eu)- 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
- Exemplos: usinas nucleares, aviões, marcapassos, sistemas eleitorais etc.
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.
- Limitar dependências:
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 installdeixar de funcionar, o próprio build do software pode se tornar impossível. - Isso pode reduzir severamente a reutilização do projeto.
- Exemplo: se
- 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.
- Ajudar a resolver problemas:
- 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.
- Escreva mais testes:
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.
- Quantidade de código e complexidade:
- 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
CreateFilenão cria um arquivo na maioria dos casos. - Esse tipo de confusão aumenta a carga cognitiva adicional e a chance de erro.
- Exemplo: a função
- 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".
- Refatore cedo e com frequência:
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."
- Prefira código ingênuo (
- 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.
- Exemplos:
- 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.
- Simplifique o código:
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.
- Use apenas em áreas experimentais:
- 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.
- Manter membros da equipe por mais de 10 anos:
- 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.
- Manter a qualidade do código por meio de revisão externa:
- 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.
- Revisões periódicas de dependências:
- 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ê."
- Um ditado dos mecânicos:
- 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
- The Practice of Programming (Brian W. Kernighan, Rob Pike)
- The Mythical Man-Month (Fred Brooks)
- A Philosophy of Software Design (John Ousterhout)
- Kill It with Fire: Manage Aging Computer Systems (Marianne Bellotti)
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
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.
O perigoso é que a SpaceX também é assim. A Tesla também...
Será que desenvolvimento guiado por currículo é o problema.
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.