Integração contínua e feature toggles

Integração contínua é uma prática de desenvolvimento de software onde membros do time integram seu trabalho continuamente em uma única base de código. Cada desenvolvedor(a) deve enviar suas alterações ao repositório compartilhado pelo menos uma vez por dia (preferencialmente a cada poucas horas).

Desta forma, o merge com o trabalho com demais integrantes do time deve ser trivial, já que o código compartilhado deve estar no máximo com apenas algumas horas de atraso.

Trabalhar com um repositório centralizado traz uma série de vantagens mas também muitos potenciais problemas. Mais uma vez, testes automatizados podem ajudar muito.

Precisamos nos sentir à vontade para incluir ou alterar novos códigos e também baixar as alterações feitas pelos demais membros do time a todo momento. Para que isso seja possível, todos devem escrever códigos automatizados (testes de unidade, de integração e ponta a ponta) para todas as funcionalidades do sistema, e nunca versionar um código que contenha algum teste falhando. O código versionado deve sempre ter todos os testes de unidade e de regressão passando. Caso algum teste falhe é nossa obrigação dar prioridade para correção destes testes.

No modelo antigo, waterfall, tínhamos uma cerimônia que precisava ser realizada para validarmos se uma nova versão do produto poderia ser publicada em produção. Com a evolução da adoção do desenvolvimento ágil e de práticas do eXtreme Programming, hoje conseguimos automatizar boa parte (ou a totalidade) deste processo de testes de regressão e validação. Com isso, diminuímos significativamente o intervalo de tempo entre o desenvolvimento e a publicação de uma nova funcionalidade em produção ou, em outras palavras, diminuímos consideravelmente o nosso lead time.

Quando juntamos as práticas de testes automatizados com a de integração contínua, chegamos em uma situação onde sempre temos em nosso repositório compartilhado uma versão do produto que pode ser implantada em produção a qualquer momento. Isso seria totalmente inviável caso não existissem testes de regressão que garantissem que os novos códigos não estão causando efeitos colaterais indesejados.

Gestão de configuração

Idealmente as alterações devem ser desenvolvidas e implantadas em produção de forma gradual. Qualquer nova funcionalidade que exija muitas alterações ou novidades deveria ser quebrada em pequenas modificações que possam ser implantadas individualmente. Desta forma, o sistema é alterado progressivamente.

Mas existem momentos que por questões técnicas ou mesmo estratégias de negócio, somos obrigados a liberar alterações no sistema com escopo mais amplo, inviabilizando a sugestão anterior. Ou seja, precisamos trabalhar em uma determinada funcionalidade durante um período superior a um dia, podendo chegar até mesmo a semanas de trabalho. Nessas situações temos duas possíveis abordagens: utilizar feature branches ou adotar feature toggles (também conhecidos como feature flags).

Feature branches

A ideia por trás dos chamados feature branches é simples: ao alterar ou iniciar o desenvolvimento de uma funcionalidade é criado um branch para trabalhar exclusivamente nesta parte da aplicação. Ao finalizar o trabalho, é feito um merge (ou pull request) para o repositório central compartilhado.

Nesta abordagem, múltiplos branchs são mantidos com desenvolvimento ativo em paralelo, por isso, o código de cada desenvolvedor(a) pode ficar bastante dessincronizado com a versão dos demais. A consequência disso é que existe um esforço e um risco alto na hora de sincronizar os branchs. Fazer estes merges é doloroso mas é a única forma de integrar o código de todos.

Feature branchs (ou qualquer outro tipo de branch) são fortemente desaconselhável quando trabalhamos com os conceitos de integração contínua. Preferimos adotar outras abordagens, como os features toggles.

Feature toggles

“Se doer, faça mais frequentemente” — Martin Fowler

No eXtreme programming quando uma prática é necessária mas ao mesmo tempo dolorosa optamos por praticá-la com o máximo de frequência possível. Quanto menor o intervalo de tempo entre os merges, menor será a dor. Embora não seja possível elimina-la por completo pelo menos será distribuída ao longo do desenvolvimento, tornando-se assim um pouco mais suportável.

Ao invés de fazer uma operação dolorosa e arriscada no final do projeto, preferimos tomar as devidas precauções ao longo do tempo para evitar maiores riscos no futuro.

Quando trabalhamos de forma integrada, podem ocorrer situações onde temos em nosso repositório compartilhado algum código que ainda não está pronto para ser entregue no ambiente de produção. Nestas situações podemos optar por utilizar feature toggles. Trata-se de um padrão simples que permite alterar o comportamento de nossas aplicações dinamicamente. É possível ativar ou desativar uma funcionalidade do sistema ou ainda mudar a implementação de uma determinada parte sempre que desejarmos. Isso possibilita que uma mesma base de código tenha comportamentos distintos dependendo da situação. Pode-se então, por exemplo, ter uma determinada funcionalidade liberada no ambiente de desenvolvimento mas indisponível no ambiente de produção.

Na prática é bastante simples de se atingir este objetivo e existem várias formas de implementar este padrão.

Como implementar feature toggles

Vamos supor que estamos trabalhando em um sistema bancário e nos confrontamos com o seguinte problema: a forma como contabilizamos o saldo da conta corrente está muito lenta. É preciso otimizar esta parte do sistema. Para isso uma analista do time teve uma ótima ideia, mas que vai demorar algumas semanas até que seja implementada e validada.

Ao invés de trabalhar em um branch, ela pode optar por criar um feature toggle chamado CALCULO_SALDO_OTIMIZADO. Desta forma o método calcularSaldo poderia ser refatorado da seguinte maneira:

Exemplo de como implementar um feature toggle.

A ideia é que o método isActive retorne true apenas no ambiente de desenvolvimento ou ainda apenas na estação de trabalho da desenvolvedora responsável pela implementação desta funcionalidade. Desta forma o código do cálculo de saldo otimizado, mesmo que incompleto, pode ser distribuído até mesmo em produção, já que nunca será executado de fato neste ambiente. Neste exemplo optei por utilizar um simples if, mas poderíamos criar uma factory, utilizar algum framework de injeção de dependências, etc.

Na prática podemos enxergar o feature toggle como uma espécie de branch, mas que é mantido na mesma base de código, o que simplifica significativamente a gestão de configuração de software.

Embora seja bastante fácil implementar toda a lógica dos feature toggles manualmente, recomendo que utilize um biblioteca destinada a essa finalidade. Algumas opção são: togglez (Java), fflip (JavaScript), NFeature (.NET), entre várias outras.

Diferentes tipos de toggles

Neste artigo utilizo o termo feature toogle de forma propositalmente genérica, com o objetivo de apenas explicar os fundamentos deste conceito. Alguns autores adotam termos mais específicos de acordo com o contexto. Dependendo do cenário podemos chama-los de release toggles, experiment toggles, ops toggles, permissioning toggles

Em sua essência e até mesmo implementação são muito parecidos, embora o propósito seja um pouco diferente em cada caso. Neste artigo utilizo o termo feature toggle para agrupar todos estes tipos de toggles.

Branchs e feature toggles devem ser sua última escolha

“Release toggle é uma técnica útil e muitas equipes usam. No entanto, deve ser sua última escolha quando você está lidando com a publicação de funcionalidades em produção. Sua primeira escolha deve ser quebrar a funcionalidade para que possa introduzir com segurança partes da funcionalidade no produto.” — Martin Fowler

Embora existam várias situações onde é tentador utilizar feature toggles ou mesmo branches, devemos evitar ao máximo estes recursos. O ideal é sempre tentar quebrar a funcionalidade em pedaços menores que possam ser implantados em produção de forma independente. Esta é a maneira mais adequada de se pensar em integração contínua.

Conseguir quebrar uma funcionalidade grande, que inicialmente parecia ser indivisível, em pequenas partes que podem ser finalizadas em questão de poucas horas é uma tarefa difícil e exige certo esforço. Mas existem recompensas!

Ao quebrar em entregas menores, o usuário final pode usufruir das melhorias mais rapidamente, não sendo necessário esperar até a conclusão de todas as partes para que possa começar a se beneficiar do trabalho que está sendo desenvolvido.

Outra consequência positiva ao se conseguir antecipar parte da entrega é que teremos um feedback mais rápido. Às vezes temos dúvidas se o caminho que estamos percorrendo é o melhor. Diminuir este intervalo de tempo e poder validar nossa solução e evolução diretamente com os usuários finais é sempre muito importante, já que podemos alterar nosso trajeto, caso necessário.

Quando colocamos uma nova funcionalidade em produção, além do feedback direto dos usuários, temos também o feedback das métricas coletadas pelos nossos sistemas de monitoração. Isso permite saber, por exemplo, qual foi o impacto na performance de cada pequena alteração realizada. Existem situações onde uma pequena alteração já traz benefícios além do esperado, mas podemos ter cenários onde ocorrem efeitos colaterais indesejados e nossas alterações tem um comportamento inverso ao esperado! É importante conseguir identificar essas situações e corrigi-las o mais rápido possível.

Conclusão

Integração contínua é uma das práticas da programação extrema que em teoria é muito fácil de aplicar: basta integrar em um único repositório central todo código-fonte do nosso sistema a cada poucas horas. Mas na vida real é um pouco mais complicado que isso. Para que seja viável realizar esta prática, é necessário que todo time estejam dispostos a implementar testes automatizados em todas as funcionalidades do sistema.

Eventuais feature toggles podem nos auxiliar em determinadas situações, mas devem ser evitados. Sempre que possível, preferimos optar por quebrar as funcionalidades maiores de forma que suas partes menores possam ser implantadas isoladamente em produção de forma gradual.