Software de sangue frio
(dubroy.com)- Alguns projetos de software são do tipo sangue quente, que só se mantêm com atividade contínua de desenvolvimento; se a atividade pode ser interrompida e retomada depois, eles estão mais próximos do tipo sangue frio
- Projetos de sangue frio escolhem tecnologia entediante para que build e testes não quebrem mesmo após longos períodos de pausa, e evitam depender de serviços externos que podem mudar ou desaparecer
- Aquisição ou encerramento de serviços dos quais se depende, upgrade de compilador e fim do suporte a pacotes acabam voltando como custo de manutenção quando se tenta reiniciar projetos com pouca atividade
- Códigos que, como projetos pessoais, ficam 1, 2 ou 3 anos sem receber atenção têm dificuldade de gerar calor contínuo, então precisam ser projetados desde o início com a premissa de uma baixa taxa de mudança
- O gerador de site estático usado no blog, desde o primeiro commit em 2012, continua funcionando quase sem alterações apenas com Python 2, 4 módulos de terceiros incluídos no repositório, execução local e deploy via
rsyncoverssh
Como manter projetos sob a metáfora dos animais de sangue frio
- Em uma aula de história natural em 2004, o professor colocou sob a câmera um filhote de painted turtle trazido do congelador e conduziu a aula assim
- O filhote de painted turtle era uma das poucas espécies capazes de sobreviver mesmo em estado congelado
- Durante uma hora, a tartaruga começou com movimentos quase imperceptíveis e, no fim, havia se deslocado cerca de metade da tela
- Animais de sangue quente precisam manter a temperatura corporal em uma faixa estreita, e humanos têm problemas ao sair de algo próximo de 37°C
- Animais de sangue frio ajustam o metabolismo à temperatura ao redor, ficam ativos quando está quente e se movem mais devagar à medida que o corpo e o ambiente esfriam
- Projetos de software também se dividem de forma parecida
- Software de sangue quente funciona bem quando há movimento e calor constantes no projeto
- Se ficar parado por 6 meses, pode parecer um projeto morto quando for retomado
Condições e exemplos de software de sangue frio
- O motivo de projetos de sangue quente ficarem difíceis de retomar é o acúmulo de mudanças externas
- O serviço do qual o CI depende pode ser adquirido ou ficar sem dinheiro e parar de funcionar
- Ao adicionar uma nova dependência, pode passar a ser necessário um upgrade de compilador
- Outros pacotes podem perder suporte e deixar de funcionar com o compilador mais recente
- Projetos em que alguém trabalha sozinho, muda algo apenas quando tem inspiração e depois passa mais de 1 ano sem tocar neles são difíceis de operar como software de sangue quente
- Um projeto de sangue frio, como o filhote de painted turtle congelado, precisa poder ser retomado daqui a 1 ano exatamente do ponto em que parou
- Para isso, usa boring technology e evita depender de serviços externos cujos scripts de build e teste podem mudar, quebrar ou desaparecer por completo
- As dependências ficam incluídas no próprio repositório do projeto, como no modelo de vendored dependencies
- O software que move este blog é um exemplo de projeto de sangue frio
- O primeiro commit foi em 8 de janeiro de 2012 e era um pequeno gerador de site estático para substituir uma instalação antiga de Wordpress
- Foi escrito em Python 2, depende de 4 módulos de terceiros, e todos eles estão comitados no repositório do projeto
- Todo o processo roda localmente, e o resultado é publicado com
rsyncoverssh - Tirando algumas pequenas melhorias, ele continuou funcionando sem modificações e a expectativa é que siga funcionando também daqui a 12 anos
1 comentários
Opiniões do Hacker News
O framework web Express, do ecossistema Node e JavaScript, ainda mantém a versão principal 4.x.x há mais de 10 anos https://www.npmjs.com/package/express?activeTab=versions
Mesmo assim, é amplamente usado, com mais de 17 milhões de downloads por semana https://www.npmjs.com/package/express, e, embora não tenha todos os recursos nem o melhor desempenho https://fastify.dev/benchmarks/, é bom por permitir desenvolvimento rápido e estável e planejamento de longo prazo
Não é preciso se preocupar com o fim de patches de segurança para versões antigas nem com mudanças bruscas de API; e Go é ainda mais estável, pois, graças à ampla biblioteca padrão e à promessa de compatibilidade, programas com mais de 10 anos ainda conseguem rodar https://go.dev/doc/go1compat
Quando surgiu, era frequentemente tratado como lixo para falsos programadores incompetentes só por ter sido escrito em JavaScript, mas depois ajudou várias empresas a criar serviços excelentes que geram dinheiro de verdade com Express, e hoje deve estar lidando com uma quantidade enorme de requisições
Hoje em dia também escrevo bastante em Go, mas ainda fico plenamente satisfeito em criar serviços com Express e, no geral, considero-o um bom software
Não é que eu realmente o odeie; é mais que provavelmente não o escolheria por causa da esteira interminável de upgrades de versão
Python é um péssimo exemplo de software de sangue-frio
Há breaking changes constantes tanto no runtime quanto nas ferramentas, e o autor também está numa situação em que precisa continuar usando Python 2, cujo suporte já acabou há muito tempo
Exemplos melhores seriam linguagens como Go ou Java, nas quais código de 10 anos atrás ainda roda bem com ferramentas modernas; ou, de forma ainda mais extrema, Perl, em que código de 30 anos atrás ainda funciona bem
Ao criar software, você acaba cometendo erros que permitem que usuários façam algo de uma forma que você não pretendia; no mundo Java, isso é resolvido adicionando recursos mais novos, seguros e com intenção mais clara, e incentivando os usuários a migrarem
Python é parecido, mas com um “e em breve vamos desligar o recurso antigo” junto; Java não faz isso
Por exemplo, o método
equalsdejava.net.URLé conhecido como um design quebrado e é fortemente desaconselhado, mas continua sendo suportado há mais de 20 anosO operador vazio do Python Airflow por um tempo aceitava o nome
DummyOperator, mas, como “dummy” já foi usado historicamente e culturalmente como termo pejorativo, os mantenedores passaram a recomendarEmptyOperatore quebraram o nome antigoAo fazer upgrade, o código dava erro no momento do carregamento até que o nome da referência fosse alterado; pessoalmente, acho que eu não quebraria os usuários desse jeito
No mundo Java, uma mudança de nome dessas poderia ser resolvida com substituição de texto, então eles provavelmente teriam continuado a dar suporte até surgir um motivo realmente insustentável para não fazê-lo
Por isso, no geral, vejo que Java e dependências de bibliotecas Java podem ser atualizados com muito mais liberdade do que Python
Se você usar uma versão Java LTS e escolher boas dependências, consegue fazer tudo rodar de novo a qualquer momento
Em Python, numa disciplina de machine learning, uma dependência introduziu da noite para o dia uma mudança de API que quebrava tudo, e o instrutor não percebeu porque estava usando a versão mais recente de algumas semanas antes, quando começou a preparar a aula
A migração de Python 2 para 3 foi uma breaking change, mas foi uma mudança única, não “breaking changes constantes”
Se você permanecer na mesma versão principal, versões menores novas não quebram código antigo; por exemplo, código 2.x antigo roda bem no 2.7, e código 3.x antigo roda bem até no 3.12
Mudanças de versão menor podem adicionar novos recursos, mas código 3.x antigo não quebra só por não usar a palavra-chave
asyncou type hintsEsse é um dos motivos pelos quais evito Python sempre que possível
Sinto que há pouca chance de código Python escrito hoje continuar funcionando daqui a alguns anos, e considero isso um problema bem grande
Mesmo tentando executar código Java de 3 anos atrás com um SDK novo, sempre havia algo quebrado
Trabalho com mainframes IBM (z/OS), e quase não vi nada que chegue tão perto da IBM em manter compatibilidade retroativa
Acho que o Microsoft Windows fica em segundo lugar, e a ABI do kernel Linux em terceiro, mas, olhando o ecossistema Linux como um todo, isso é apenas uma pequena parte
A maior parte do restante se aproxima mais de churn, e no open source parece raro alguém querer gastar seu tempo livre com compatibilidade retroativa
Economicamente, isso parece um dilema do prisioneiro: todos empurram o custo de manter compatibilidade para os outros e, no fim, criam mais trabalho inútil para todo mundo
Por exemplo, olhando para a comunidade de computação retrô, não é raro escrever drivers para fazer hardware novo funcionar em sistemas operacionais antigos
Sem remuneração, no fim depende de quanto você valoriza a plataforma que está criando, e eu decidi mirar diretamente o kernel Linux por meio de chamadas de sistema por causa do compromisso comprovado dele com estabilidade de ABI
Por outro lado, com a linguagem de programação que eu mesmo criei, fico com vontade de continuar corrigindo coisas porque quero deixá-la o mais “perfeita” possível
Coloquei um aviso no README dizendo que ela ainda está em estágio inicial de desenvolvimento e é instável, caso alguém queira usá-la de forma insana
Imagino que quem cria Ruby ou Python se sinta de modo parecido; a linguagem parece um filho, você quer que ela tenha sucesso, então pode achar que precisa corrigir erros como o fato de
printter sido uma palavra-chaveMuitas vezes, inclusive, o que quebra são justamente as partes voltadas à compatibilidade retroativa
No meu emprego anterior, criávamos apps Node conteinerizados, e o CI construía as imagens a partir do código-fonte Node; de repente, os deploys de serviços que não eram mexidos havia um tempo começaram a falhar
Descobrimos que o Dockerfile se baseava em uma imagem do Ubuntu cujo período de suporte havia acabado, e os repositórios de atualização tinham sido movidos para repositórios arquivados, então não dava para construir a imagem sem corrigir o Dockerfile
É um exemplo de software que quebra mesmo sem ser tocado, e por isso prefiro Go e binários únicos
Se você empacotar como release, nem precisa recompilar, e uma imagem Docker Distroless não tem dependências além do meu binário
Usei Go por muito tempo e nunca tive problemas de software se deteriorando com a idade; vários tipos de problemas que eu sentia ao usar Node ou PHP desapareceram
No lado do Node, o segundo maior problema são os padrões de indireção dos frameworks, e o primeiro é o gerenciamento de pacotes
Problemas de peer dependency como “instalei a versão X, mas o módulo Y precisa da versão Z” aparecem o tempo todo
Muitos engenheiros, ao procurar bibliotecas no GitHub, verificam a data do último commit
Há uma tendência a pensar que, quanto mais recente o commit, melhor suportada é a biblioteca
Mas, se um projeto arquivado faz exatamente o que você precisa, tem zero bugs e foi estável por anos, é como encontrar uma joia escondida em um brechó
Hoje em dia, muitos engenheiros descartam automaticamente bibliotecas que não são atualizadas “continuamente”, e parecem achar que isso é algo bom
O ambiente moderno de desenvolvimento de software muitas vezes não é assim, e o front-end web é um exemplo típico de mudanças frequentes
Uma biblioteca totalmente independente pode ficar bem sem atualizações, mas uma biblioteca que depende de um framework de front-end web causará problemas se não for atualizada para acompanhar as mudanças do ecossistema
Não sei os números reais, mas acredito que, na esmagadora maioria dos casos, ausência de atividade recente significa “abandonado”, não “concluído e sem bugs”
Algumas linguagens ficaram quase irreconhecíveis em relação à versão 1.0, enquanto outras mantiveram a maior parte do código escrito e apenas adicionaram coisas por cima
No fim, essa tendência parece se refletir também na comunidade e no ecossistema
Lembro que Clojure aparecia perto do topo da lista por quase não fazer mudanças que quebram compatibilidade, e uma biblioteca cuja última alteração foi há 5 anos ainda roda perfeitamente na versão atual da linguagem
O fato de ser da família Lisp, permitindo estender o núcleo da linguagem sem mudanças upstream, também parece ajudar, embora naturalmente isso tenha seus próprios defeitos
Ainda assim, foi bom porque me fez parar de pensar que “frescor” é igual a “qualidade”
Hoje em dia uso com mais frequência bibliotecas que quase não mudam há anos do que bibliotecas criadas no ano passado, e não tenho grandes problemas
Algumas linguagens lançam versões a cada 1 ou 2 anos e acrescentam sintaxes novas e elegantes ou tipos abstratos de dados na biblioteca padrão para substituir padrões usados com frequência, mas que eram desajeitados
A comunidade dessa linguagem passa a considerar a nova sintaxe “idiomática” quase imediatamente, e entende que código escrito no antigo jeito mais pesado deve ser corrigido
O motivo para mudar uma base de código específica geralmente é que o jeito antigo é mais obscuro em comparação com a nova sintaxe e dificulta manutenção e code review
Se a nova sintaxe existisse desde o começo, ninguém acharia que o jeito antigo era bom código; portanto, o argumento é atualizar o código para melhorar a legibilidade para novos desenvolvedores e reduzir a barreira de entrada para contribuições
Se uma biblioteca implementada em uma linguagem desse tipo não é atualizada há mais de 3 anos, isso muitas vezes é um mau sinal
Pode significar que o desenvolvedor não está conectado à comunidade o suficiente para manter o código em forma idiomática e facilmente legível por outros desenvolvedores que aprenderam a forma mais moderna da linguagem, e talvez nem tenha interesse em aceitar PRs externos
Pode haver vulnerabilidades de segurança; ninguém as reporta apenas porque o projeto está marcado como abandonado
O único software que consegue viver sem atualizações é aquele que foi feito corretamente desde o início
Se for um software só para você, isso é relativamente fácil: é bem provável que seus gostos não mudem muito mesmo depois de 10 anos e, como
né pequeno, dá para ignorar problemas pequenos em que se usa uma funçãoO(n^2)mesmo havendo umaO(n)Mas, se for um software que outras pessoas vão usar, os requisitos são diferentes, e surgem problemas do tipo em que, para um
Ngrande o suficiente, uma funçãoO(n)passa a ter valorSeja para uso próprio ou para os outros, podem aparecer problemas imprevistos
Por exemplo, ao processar arquivos com mais de 1 GB, ele pode travar, mas você nunca ligou para isso porque normalmente só usava arquivos com menos de 100 KB; ao tentar corrigir, talvez tenha que reescrever metade do sistema
A maior objeção à ideia de que software que não muda é inerentemente melhor do que software que muda com frequência está aí
Um software que não muda pode ter sido perfeito desde o início, mas também pode haver um horror escondido nas profundezas, e é difícil distinguir isso de antemão
Isso também não quer dizer que software atualizado rapidamente seja inerentemente melhor do que software atualizado lentamente; há muitos fatores além da velocidade das atualizações
Se os requisitos mudam, é claro que o software também deve mudar
Mas, ao longo de 10 anos, muita coisa pode acontecer sem relação com mudanças de requisitos
Projetos open source podem ser abandonados ou mudar de direção, softwares comerciais podem ser descontinuados, empresas podem ser adquiridas, regras da App Store ou da Play Store podem mudar, APIs podem desaparecer ou mudar de preço a ponto de destruir a viabilidade econômica de um projeto
Toolchains, frameworks, linguagens de programação, paradigmas e boas práticas também mudam
Acho que o ponto central é impedir que mudanças externas, sem relação com os requisitos, me obriguem a mudar
É um bom princípio, mas, como sempre, há trade-offs
Ser estável é diferente de ficar obsoleto, e essa diferença muitas vezes se decide na segurança
E se for fácil atender a um novo requisito importante, mas para isso for preciso atualizar uma biblioteca vendored em 7 versões principais, causando um monte de quebras não relacionadas?
E se já não houver pessoas suficientes familiarizadas com um conjunto de ferramentas parado no tempo, e ninguém quiser aprendê-lo?
Escolher dependências com cuidado e conservadorismo é bom, mas acho que deixar de acompanhar até mesmo as mudanças dessas poucas dependências mantidas pequenas já é ir um passo longe demais
Concordo com o sentimento do texto
Odeio o fato de que até um app mobile feito há apenas alguns anos agora exige dezenas de horas para ser corrigido e ter uma atualização enviada
Também é interessante a parte final em que o autor chama seu gerador de site estático de software de sangue frio e diz que ele roda em Python 2
Python 2 está ficando cada vez mais difícil de instalar hoje em dia, e no fim esse projeto também vai se tornar um projeto de sangue quente
Toda vez que atualizo o Xcode, preciso corrigir coisinhas para que o projeto compile e funcione de forma limpa; isso é realmente irritante e deveria ser totalmente inaceitável
As mensagens recentes do histórico do git são todas variações de “corrigido para funcionar no Xcode mais recente”
Se essas mudanças no SDK ou no sistema operacional subjacente fossem necessárias por ameaças de segurança, eu até entenderia em alguma medida, mas quase nunca é o caso
Na maior parte das vezes são mudanças idiotas como descontinuar APIs, adicionar warnings por padrão e dizer que agora se deve usar este framework em vez daquele
Plataformas e frameworks precisam parar de ser alvos móveis de propósito, especialmente quando já são sistemas operacionais muito estáveis e confiáveis
Deveria ser possível tirar um projeto de 10 anos do congelador e fazê-lo compilar e rodar tão limpo quanto há 10 anos
Esses fornecedores de sistemas operacionais são empresas de trilhões de dólares, então não quero ouvir a desculpa de que manter compatibilidade retroativa exige muito esforço de engenharia
Continuo mantendo um projeto pessoal paralelo
Comecei há 12 ou 13 anos em PHP puro; depois reescrevi em Laravel e, por volta de 2017, reescrevi de novo em Symfony
Houve períodos de 6 a 18 meses em que, por trabalhar full-time como freelancer, eu não tinha energia e fazia só 2 ou 3 commits muito pequenos, mas, quando tinha tempo, adicionava funcionalidades, fazia upgrades, experimentava e aprendia
Foi muito útil para aprender a manter um projeto no longo prazo
Aprendi coisas como atualizar dependências, remover o desnecessário, verificar atualizações de segurança, procurar oportunidades de simplificação (de Vagrant para Docker, de Vue + Axios + Webpack etc. para Htmx) e também aprendi o que evitar
Pessoalmente, passei a evitar dependências recém-criadas, microsserviços e infraestruturas complexas como Kubernetes
Recentemente criei várias funcionalidades, atualizei para PHP 8.2 e Symfony 7 e também integrei recursos baseados em ChatGPT, então acho que, se eu quiser, posso descansar por 1 a 3 anos
Nos últimos 4 ou 5 anos, esse projeto gerou uma receita parecida com a renda anual média de um freelancer, então não é um projeto paralelo obscuro adormecido
Depois de alguns anos sem usar, voltei e vi que aquelas funções horríveis de manipulação de imagens continuavam exatamente como estavam quando saí, 8 anos antes
Além do que foi dito no texto, um modelo de ameaças inerentemente seguro é importante
Por exemplo, um site inteiro precisa lidar continuamente com atacantes e spambots, então é inerentemente mais próximo de sangue quente
Por outro lado, uma página estática como TiddlyWiki pode nem sequer ser colocada na web, e o navegador é uma plataforma extremamente estável, então é muito melhor
A diferença de preferência entre projetos de sangue-frio e projetos de sangue-quente parece estar relacionada ao Buxton Index mencionado em https://www.cs.utexas.edu/users/EWD/transcriptions/EWD11xx/EWD1175.html
Uma pequena mercearia de bairro teria cerca de 0,5 ano; um verdadeiro cristão, infinito; um político médio de olho na reeleição, cerca de 4 anos; a maior parte da indústria, um pouco mais do que isso; e gestores que precisam escrever relatórios trimestrais, bem menos
O motivo pelo qual o Buxton Index é importante é que a cooperação estreita entre sujeitos com Buxton Indexes muito diferentes inevitavelmente fracassa e leva a acusações morais
O lado com prazo mais curto é acusado de ser superficial e míope, enquanto o lado com prazo mais longo é acusado de negligência profissional, fuga de responsabilidade ou de pegar carona
Também acabam considerando um ao outro burro
A vantagem do Buxton Index é que, por ser um conceito numérico simples, ele é moralmente neutro e eleva a diferença acima de uma disputa moral
Isso é especialmente importante ao pensar em colaboração entre academia e indústria
Esse nome é muito ruim
Animais de sangue-frio dependem muito do ambiente, enquanto animais de sangue-quente reduzem a dependência da temperatura externa por meio do metabolismo
De qualquer forma, é desnecessariamente ambíguo
Se simplesmente chamasse de “software sem dependências externas”, daria para eliminar o parágrafo explicativo prolixo
Eu já não gosto de textos de desenvolvimento de software que pulam para conclusões rasas com metáforas inadequadas tiradas da natureza; gosto menos ainda quando fazem isso entendendo completamente errado o próprio fenômeno natural usado na metáfora
O fato de algumas espécies, incluindo tartarugas-pintadas, sobreviverem ao congelamento não se deve ao sangue-frio, mas a proteínas anticongelantes especiais
Outros lagartos ou animais de sangue-frio teriam seus próprios tecidos rompidos ao descongelar
https://lobste.rs/s/hitos3/cold_blooded_software#c_mxjzwh