Desenvolvimento de Software de Baixo Custo de Manutenção
Objetivos
- Sem dívida técnica.
- O software é projetado para ser facilmente compreendido, e quando precisar ser modificado, o código correspondente será fácil de encontrar.
- O software tem a manutenibilidade como prioridade máxima, permitindo que os desenvolvedores voltem a melhorá-lo anos depois de tê-lo esquecido.
- O software se aproxima infinitamente do estado "concluído", sendo satisfatório até que se torne obsoleto devido a mudanças de ambiente ou novas necessidades.
- É possível atualizar com confiança dependências com a mesma versão principal.
- Quando o software for descontinuado, ele pode ser removido suavemente dos sistemas existentes.
Princípios
Lembre-se: "não existe almoço grátis". Não se pode obter um projeto de software de baixo custo de manutenção sem fazer nada. A chave para um desenvolvimento de software de baixo custo de manutenção não é cometer menos erros, mas sim fazer cada pequena coisa certa, o que é, sem dúvida, bastante desafiador.
Aqui estão alguns princípios gerais que ajudam a alcançar o desenvolvimento de software de baixo custo de manutenção:
- Descartar todas as funcionalidades não essenciais.
- Parar de usar linguagens e cadeias de ferramentas com ecossistemas não saudáveis.
- Usar linguagens com forte poder expressivo, evitando distorcer e desviar dos padrões comuns da linguagem durante a codificação.
- Parar de usar macros para qualquer propósito que possa gerar code smells.
- Usar apenas linguagens com definições de tipo ou anotações de tipo.
- Usar, sempre que possível, tecnologias testadas pelo tempo, pois estas têm maior probabilidade de existir por mais tempo.
- Pensar globalmente, evitando a programação tática.
- Refatorar o código ativamente, permitindo um grande número de modificações de linhas de código para reduzir a entropia do software.
- Cortar designs menos necessários, transferindo a complexidade para o usuário, pois designs "humanizados" são frequentemente os mais difíceis de manter. Existem muitos exemplos semelhantes na realidade. Consideremos os editores Markdown e os editores WYSIWYG: O primeiro exige um (mínimo) custo de aprendizado do usuário, enquanto o segundo quase não tem custo de aprendizado. No entanto, o segundo é muito pior em termos de manutenibilidade:
- Editores Markdown facilitam a escrita de arquivos de texto puro com estrutura clara e legíveis por humanos. Editores WYSIWYG produzem modelos de documentos internos difíceis de migrar ou HTML completamente caótico.
- Documentos Markdown ainda poderão ser usados em 2050, e editores Markdown tendem a ser minimalistas e substituíveis. Editores WYSIWYG são mais propensos a se tornarem obsoletos, mais difíceis de manter a compatibilidade com versões anteriores e mais complexos para os desenvolvedores manterem; os fornecedores podem muito bem não existir em 2050.
- Parar de usar grandes frameworks de software que podem levar ao aprisionamento por fornecedor (vendor lock-in). Quando um framework de software tenta se adaptar a todos os ambientes ou tenta fornecer uma solução one-stop, ele deixa de ser um framework e se torna uma bomba-relógio.
- Reduzir ao máximo a dependência de pacotes de software de baixa qualidade. Isso significa realizar uma revisão da qualidade do código das dependências. Felizmente, estilos de código ruins são fáceis de identificar e muitas vezes não exigem a compreensão real de como o pacote funciona para fazer um julgamento.
- Não desenvolver software para plataformas que não têm compatibilidade com versões anteriores.
- A menos que o software tenha uma única funcionalidade, deve-se tentar evitar que ele seja multiplataforma.
- Proibir o uso de serviços proprietários oferecidos por plataformas de cloud computing.
- Proibir o uso de gerenciadores de pacotes que não podem bloquear as versões das subdependências, e proibir o uso de repositórios Git como fonte de pacotes de software.
- Se não for estritamente necessário, não se deve aderir aos padrões e protocolos existentes:
- A maioria dos padrões e protocolos é lixo, e grande parte deles nem sequer tem valor de referência. Aderir a eles só prejudicará o desenvolvimento do software. Por exemplo, você pode infelizmente encontrar requisitos que não podem ser implementados com esses padrões e protocolos, ficando em uma situação difícil.
- Aqueles padrões e protocolos muito populares para serem ignorados, sua popularidade geralmente deriva de sua vantagem de pioneirismo e da garantia de compatibilidade com versões anteriores. Isso faz com que suas implementações em produção estejam repletas de casos de uso estranhos e de borda, e coletar e lidar com esses casos de borda pode ser exaustivo.
- Prestar atenção ao nível de abstração da cadeia de ferramentas e parar de usar cadeias de ferramentas difíceis de migrar ou atualizar.
- Escrever muitos testes de integração automatizados para aumentar a sensação de segurança ao atualizar as dependências.
- O ambiente necessário para a execução do software deve ser dedicado e isolado, não compartilhado com outros projetos.
- Valorizar a autonomia tecnológica, removendo, sempre que possível, dependências que possam ser afetadas por fatores externos.
- Reduzir ao máximo o número de desenvolvedores que não sejam os desenvolvedores principais, para evitar que o atrito na colaboração prejudique a eficiência do desenvolvimento do projeto. Isso é como um engarrafamento: a melhor solução não é alargar a estrada, mas reduzir o número de carros circulando ao mesmo tempo.
Reinventar a roda
Reinventar a roda é um anti-padrão, pois sempre há uma ou mais rodas já inventadas esperando para serem reutilizadas. No entanto, a reutilização de rodas existentes frequentemente gera problemas de gerenciamento de dependências: as coisas começam bem, mas os problemas logo surgem. O caminho mais simples é frequentemente o caminho que maximiza a dívida técnica. Ao seguir esse caminho, é comum sentir frustração. Com o tempo, mesmo as pessoas mais confiantes e apaixonadas desenvolvem um desamparo aprendido, acreditando que o desenvolvimento de software é uma tarefa interminável e tediosa.
Julgar se uma dependência vale a pena ser introduzida é uma habilidade importante e facilmente negligenciada, que exige muita experiência indizível para ser sustentada. Quando você carece dessa habilidade, é fácil ser enganado e se tornar cúmplice de futuras vítimas ou mesmo agressores. Como a maioria dos desenvolvedores carece dessa habilidade, o fenômeno de "moedas ruins expulsam moedas boas" é muito comum na indústria.
Projetos de software open source que atingem um certo tamanho podem facilmente se transformar em "montanhas de lixo":
- Abstrações erradas, arquitetura ruim, superfície de API terrível.
- Falta de testes convincentes.
- A documentação não é mantida ativamente, é inconsistente com a realidade, ou esconde/ignora detalhes úteis.
- Muita codificação tática, pouca codificação estratégica; o volume de código adicionado é sempre maior do que o volume de código removido.
- Gasto excessivo de tempo com requisitos inexistentes, levando ao inchaço e à deterioração do software.
- ……
A menos que haja mantenedores autoritários na comunidade que supervisionem com uma perspectiva estratégica e executem rigorosas revisões de código, o modelo de participação em software open source naturalmente criará essas "montanhas de lixo", independentemente do nível dos contribuidores. O software closed source tem os mesmos problemas, mas a opacidade do closed source os esconde ainda mais; problemas varridos para debaixo do tapete não significam que desapareceram.
Para se libertar da dívida técnica, às vezes você só pode usar o chapéu NIH. Ao reinventar a roda, você pode notar que, além de nos libertar de dependências de terceiros ruins, reinventar a roda tem benefícios que os desenvolvedores atolados em dívida técnica nunca poderão conhecer:
- Há uma chance de inventar uma roda melhor.
- Você se tornará cada vez mais hábil em inventar soluções.
- Você se tornará cada vez mais capaz de discernir a qualidade das dependências.
Quando você tem como objetivo um baixo custo de manutenção, os benefícios de reinventar a roda podem ser ampliados no futuro: depois de se livrar da dívida, o que resta são os juros – que conceito simples. Com o tempo, a acumulação desses feedbacks positivos fará com que a opção de reinventar a roda ocupe um lugar cada vez mais importante em sua mente. No entanto, antes de reinventar a roda, deve-se sempre avaliar cuidadosamente sua viabilidade econômica. Reinventar tudo ainda não é aconselhável; obviamente, algumas rodas têm uma relação custo-benefício muito baixa para serem reinventadas, e o risco de falha no desenvolvimento é alto, então você deve ter cuidado.
Construir dependências estilo bateria
Algumas linguagens de programação e frameworks afirmam vir "com baterias incluídas" (batteries included), o que significa que eles têm funcionalidades avançadas embutidas que podem ser usadas diretamente. A essência dessas "baterias" são APIs de alto nível, relativamente estáveis, que evitam ao máximo expor detalhes de baixo nível; a abstração e os testes corretos garantem que raramente ocorram mudanças disruptivas.
O conceito de "baterias" pode ser totalmente aplicado ao gerenciamento de dependências, permitindo a criação de um tipo de "dependência estilo bateria": Essa dependência atua como uma camada intermediária entre o projeto que a utiliza e a dependência em si, fornecendo um conjunto de APIs ordenadas e limpas baseadas na dependência. Isso permite evitar que o código de negócios use diretamente funções ou classes com parâmetros gigantescos ou em grande quantidade, inventadas por "bárbaros".
Como você se concentra apenas nas partes da dependência que são realmente usadas, você pode escrever seu próprio código de teste na camada intermediária para garantir que o comportamento da dependência permaneça conforme o esperado quando sua versão for atualizada. Quando a dependência se tornar obsoleta no futuro, você pode optar por usar uma versão antiga, substituí-la ou implementá-la você mesmo, usando os testes previamente escritos para garantir a eficácia da refatoração. Essa é a principal diferença entre as baterias que você constrói e as que outros constroem: você sempre pode recarregar ou expandir suas próprias baterias, enquanto para as baterias de outros, geralmente você só pode vê-las esgotar-se lentamente.
Devido à similaridade formal, as dependências estilo bateria e a lógica de negócios encapsulada podem ser confundidas. A principal diferença entre elas está na distância em relação à dependência. As dependências estilo bateria geralmente estão muito próximas da dependência, enquanto a lógica de negócios geralmente está muito distante da dependência. Apenas em algumas situações, como quando a lógica de negócios é construída diretamente sobre a dependência relevante, as duas se tornam semelhantes.
As dependências estilo bateria parecem ser o Santo Graal para resolver problemas de gerenciamento de dependências, mas ainda apresentam vários problemas:
- Afinal, é uma camada intermediária adicional, o que torna mais complicado do que usar a dependência diretamente quando você precisa de novas funcionalidades ou precisa fazer mudanças disruptivas.
- É difícil construir a abstração correta. No caminho para encontrar a abstração correta, às vezes é inevitável ter que começar do zero. As pessoas também costumam ter pouca previsão das necessidades futuras, então uma abstração que parece correta em um momento pode não parecer mais no futuro. As pessoas temem que os projetos fiquem presos em um ciclo de modificações devido a abstrações erradas, então preferem se envolver em um turbilhão maior, onde pelo menos é mais fácil culpar os outros quando algo dá errado; é difícil refutar essa ideia.
Usar pacotes pequenos
Pacotes pequenos são a manifestação conjunta da filosofia Unix e do versionamento semântico. Quanto menor a granularidade de um pacote, menos funcionalidades um único pacote terá; quanto menos funcionalidades, mais estável ele será, e menos coisas serão quebradas ao modificar o código.
Pacotes pequenos às vezes podem trazer novos problemas, como problemas de gerenciamento decorrentes do aumento do número de pacotes. Esses problemas podem ser complicados, pois, com o tempo, tornam-se cada vez mais difíceis de resolver pela força bruta. Essa tendência acaba forçando o surgimento de soluções especializadas, como codemods do Facebook e similares, mas usar essas ferramentas ainda é problemático.
Entre legibilidade e performance, escolha legibilidade
Na codificação prática, surgem situações em que há um dilema entre legibilidade e performance. Nesses momentos, os desenvolvedores tendem, por miopia, a escolher performance em vez de legibilidade. A escolha da performance geralmente não tem uma razão forte para sustentá-la, e o trade-off de sacrificar a legibilidade por performance raramente compensa a longo prazo. Em alguns casos, os desenvolvedores escolhem a performance apenas por um ganho psicológico infantil de "escrever código de alta performance pode fazê-los parecer inteligentes".
Quando estiver em dúvida entre legibilidade e performance, você deve sempre escolher a legibilidade; isso não pode ser enfatizado o suficiente. Tem certeza de que este código será escrito apenas uma vez e nunca mais será alterado? Escolha a legibilidade. Código mais legível é 1000 vezes mais lento? Escolha a legibilidade. Se houver um impacto negativo observável na performance, então escreva outra versão do código: Esta versão será a de alta performance, transpilada a partir do código altamente legível, e você precisará escrever uma documentação muito detalhada ao lado para explicar como este código foi gerado.
Uma exceção é quando você precisa escrever um programa de alta performance; como seu objetivo inicial é a alta performance, a legibilidade como opção desaparece. No entanto, mesmo em programas de alta performance, ainda há oportunidades para melhorar a legibilidade do código, embora essas oportunidades sejam difíceis de serem percebidas —geralmente, só na segunda vez que o código é escrito é que se percebe. E uma vez que se percebe essa oportunidade de melhorar a legibilidade, não há razão para continuar a ignorá-las —mantendo a performance inalterada, deve-se sempre aumentar a legibilidade ao máximo possível.
Apreciar o software que não precisa de manutenção
A "ausência de manutenção" do software deve ser vista e apreciada como uma forma de beleza. Quanto mais longo o "tempo sem manutenção" do software, mais tempo sua "qualidade sem manutenção" é testada, e mais beleza se acumula.
A parte mais crucial é que você sabe claramente que o software não é impossível de manter, nem difícil de manter, mas sim que não precisa de manutenção. O código do software está lá, mas você não precisa mexer nele, porque ele ainda atende às necessidades, funciona normalmente e funciona muito bem.
Mesmo quando chega o momento de precisar de manutenção, a resistência à manutenção do software deve ser como um papel de janela, facilmente eliminada com um leve toque. Como não há dívida técnica, você não precisa pagar juros. A obsessão por atualizar dependências ataca? Sem problemas, porque essa atualização pode ser feita em dez minutos, e você tem muitos dez minutos para "desperdiçar". Mas melhor ainda é você se vencer e conter-se a tempo —não apenas porque ainda não surgiram novas necessidades, mas também porque você não quer destruir a beleza que o software acumulou ao longo do tempo.
Desenvolver essa capacidade de apreciação é bastante difícil, pois é muito contra-intuitivo. As pessoas, subconscientemente, confundem erroneamente a novidade de algo como um fator para medir sua qualidade —estar na moda às vezes parece mais importante do que qualquer outra coisa —um pouco de reflexão revelará que isso não é racional. Apenas na vida cotidiana, pode-se notar que dispositivos bem construídos e com tecnologia madura são frequentemente mais duráveis do que o esperado —eletrodomésticos que funcionam ininterruptamente, como geladeiras, são o exemplo mais típico; poucas pessoas trocam de geladeira anualmente. O software, como produto digital, não tem razão para se tornar obsoleto mais rapidamente que o hardware; a facilidade de atualização não deveria ser a principal razão para atualizar.