Arquivo para julho \23\UTC 2008

Por um controle de versão menos insano

Recentemente encontrei um texto do Jeff Atwood sobre a instalação do Subversion. Ele tem alguns argumentos bastante interessantes sobre controle de versão (sempre use controle de versão, faça modificações pequenas e blá, blá, blá…) que me parecem em geral bastante acertados, mas uma coisa me chamou a atenção: por que um programador (que pelo menos parece) tão bem informado quanto ele ainda escolhe um sistema de controle de versão tão completamente insano quando já existem tantas alternativas mais naturais para qualquer ser humano?

A maioria dos sistemas da velha guarda incrementa algum tipo de contador para identificar cada versão do objeto controlado: o Subversion, por exemplo, gera números de revisão quando há modificações em alguma parte da árvore de diretórios e o CVS associa um identificador seqüencial a cada um dos arquivos controlados. O maior problema do versionamento seqüencial é que simplesmente não controla o que queremos que controle. Saber quantas vezes um item qualquer foi modificado é útil, mas não tão útil quanto saber quais foram as modificações e, ainda melhor, quais as dependências entre elas. Sim, é bastante interessante saber qual o estado final de um trecho de código após um certo número de transformações, mas me interessa muito mais saber o que cada uma dessas modificações faz e tratar as modificações como objeto de trabalho, não os resultados delas.

Mais do que quantas modificações aconteceram desde algum ponto qualquer do tempo, eu preciso saber se a segunda modificação pode ser aplicada sem a primeira, se a terceira é uma inversão da primeira ou se a quinta desfaz algumas coisas que a quarta fez. As respostas para todas estas perguntas são necessárias para a operação mais fundamental do controle de versão: o merge.

Quando o Subversion surgiu, lá pelos idos de 2004, um de seus maiores argumentos era o branch barato. Este com certeza é um bom recurso para se ter, afinal de contas derivações precisam ser criadas o tempo todo. Mas o que queremos mesmo é que o merge seja fácil. O branch é só o começo da história. O merge é o ponto alto do processo, é quando as contribuições dos vários envolvidos são combinadas e passam a (pelo menos tentar) funcionar em conjunto. Há uma palestra em que Linus Torvalds resume isto muito bem em algumas poucas palavras: “branches são completamente inúteis, a não ser que você faça o merge”. Só que o versionamento seqüencial pára no branch e o merge precisa ser feito de forma manual porque o sistema não enxerga as dependências entre as alterações, só sabe qual delas aconteceu antes ou depois. Você pode conseguir ajuda para comparar duas versões do mesmo conteúdo, mas é você quem tem que saber quais devem ser as duas versões para este merge em particular que você quer fazer. A coisa fica ainda mais complicada quando você precisa importar modificações de outro branch periodicamente. Quando, por exemplo, seu projeto tem um branch estável em que só se faz conserto de bugs e outro — ou outros — em que são desenvolvidos novos recursos e que precisa receber as mesmas modificações que o estável para se manter livre de bugs. As pessoas acabam desenvolvendo algumas soluções arcanas para controlar manualmente quais alterações já foram aplicadas a quais linhas de desenvolvimento, coisa que o sistema de controle de versão podia (e deveria) fazer sem precisar de babá.

Os identificadores seqüenciais precisam ser atribuídos por algum tipo de servidor central. Não podem ser determinados por máquinas diferentes para evitar conflitos de nomes e acabam sendo completamente artificiais porque refletem simplesmente a ordem em que o servidor recebeu as alterações. Para a maioria das alterações, a ordem de aplicação pouco importa. E quando ela importa, os sistemas com versionamento seqüencial não costumam ajudar.

A alternativa ao controle seqüencial é o controle de patches (termo que admito estar inventando agora, então não deve ser muito preciso e provavelmente você vai encontrar isto com outro nome por aí). Um sistema de controle de patches associa um identificador a uma modificação, não ao resultado dela como os de controle seqüencial. Uma versão qualquer é simplesmente um acumulado de modificações que, quando combinadas, determinam um resultado final.

Sistemas seqüenciais são necessariamente centralizados. Não há como decidir qual o número para a próxima versão se não houver uma autoridade central para controlar a numeração. Porém, o inverso não é verdadeiro. Isto é, sistemas baseados em controle de patches não precisam ser distribuídos. A maioria dos que encontramos realmente é, mas não precisavam ser. A questão é que eles podem ser muito bem usados como sistemas centralizados, mas já tem tudo que precisam para serem distribuídos e independentes de um servidor central. Então porque se limitar? Além de poderem fazer tudo que os centralizados conseguem, os sistemas distribuídos ainda costumam ser muito mais fáceis de instalar. Se o Atwood tivesse escolhido um deles, não precisaria nem escrever um tutorial de instalação para si próprio. Era só escolher inicializar um diretório qualquer como repositório e começar a versionar o que quisesse. Sem serviços. Sem nomes de usuário. Sem senhas. Sem dores de cabeça com o firewall.

Minha impressão é que o funcionamento dos sistemas distribuídos de hoje é muito mais parecido com o modo como naturalmente pensamos do que o dos centralizados. Quando quisesse combinar duas linhas de trabalho distintas eu deveria apenas dizer a meu sistema que quero combinar as modificações que eu tenho com as que meu colega tem e ele deveria ser capaz de fazer isso sozinho (assumindo que não haja conflitos complicados). Eu não deveria precisar criar marcadores artificiais e manter manualmente um histórico de quais alterações dele eu já tenho. Por que algumas vezes ainda insistimos em usar sistemas centralizados? Há alguma vantagem oculta neles ou o quê?

Seja bem-vinda, manutenção!

É bastante comum ver organizações que costumam desenvolver software em cascata espernear quando chega a hora de implantar e colocar os sistemas em produção. Depois que tudo está instalado e funcionando a todo vapor elas acabam entrando em modo bombeiro e passam simplesmente a apagar um incêndio atrás do outro, sem saber muito o que fazer e como organizar seus esforços de manutenção.

Isto costuma acontecer porque a abordagem de desenvolvimento delas simplesmente não está adaptada à manutenção. Ela foi otimizada para um cenário (completamente fictício, diga-se de passagem) onde não há vida após a entrega do sistema, onde os clientes não mudam de idéia e onde não acontecem requisições de mudança depois que o software está pronto.

Não importa quanto alguém se dedique à tarefa. Ninguém consegue fazer a água da cascata cair para cima.

A abordagem oposta, obviamente, é otimizar para a manutenção, isto é, estar pronto para começar pequeno, mudar sempre que necessário, consertar o software aos poucos e torcer para que um dia ele não precise mais de conserto. A proposta Ágil começou com experiências neste sentido, tomando emprestado da filosofia release early, release often e preferindo usar o próprio software para comunicação entre os clientes e a equipe de desenvolvimento ao invés de documentos e diagramas. Num certo sentido, equipes ágeis tomam a manutenção como modo de operação normal no lugar do desenvolvimento puro. Preferem desenvolver uma solução completa e usável para um problema pequeno rapidamente para que possam dar as boas vindas à manutenção o mais cedo possível. Enquanto os clientes não tem uma peça real de sofware para usar e experimentar, suas sugestões são só um pouco melhor do que especulação.

Esta não é uma idéia tão louca no fim das contas. Pensando bem, da segunda linha de código para a frente tudo é manutenção.