Fundamentos de Manutenção de Software
9 Processos 🔗
Agile processes harness change for the customer’s competitive advantage. ― Agile Manifesto, 2001
Neste capítulo, vamos tratar de como manutenção de software é contemplada em processos modernos de desenvolvimento de software. Primeiro, na Seção 9.1, vamos comentar sobre a visão que métodos ágeis possuem sobre mudanças de software e sobre a importância de ter times autônomos, capazes de desenvolver, manter e evoluir seus sistemas. Em seguida, na Seção 9.2, vamos apresentar algumas práticas — relacionadas diretamente com manutenção e evolução de software — que esses times costumam realizar com frequência, incluindo revisão de código, slacks, post mortems, congelamento do código (code freeze) e recompensas por descoberta de bugs (bug bounties). Depois, na Seção 9.3, vamos comentar sobre métricas que os times podem usar para acompanhar o trabalho que está sendo realizado, incluindo métricas de fluxo, qualidade, código e negócios. Por fim, vamos comentar sobre dois temas mais operacionais, relacionados com a forma de uso de sistemas de controle de versão. Especificamente, na Seção 9.4, vamos tratar de modelos de branches, incluindo Git-flow, GitHub Flow e Desenvolvimento Baseado no Trunk (TBD). Para concluir o capítulo, na Seção 9.5, vamos apresentar um padrão para escrita de mensagens de commits, chamado de conventional commits.
9.1 Introdução 🔗
Atualmente, os processos de desenvolvimento de software mais comuns são ágeis, tais como Scrum, Kanban ou XP. Uma parte do sucesso desses processos se explica pela compreensão que eles tiveram sobre o papel de mudanças em desenvolvimento de software. Ou seja, software, como o próprio nome sugere, é flexível e pode ser adaptado de forma mais simples e barata do que a maioria dos produtos físicos.
Imagine um edifício residencial: é quase impossível realizar mudanças significativas nele depois de pronto. Normalmente, não dá para construir um novo andar ou aumentar o tamanho dos apartamentos. Porém, mudanças e evoluções em software são mais viáveis, pelo menos em linhas gerais. Por isso, na maioria dos sistemas, não vale a pena investir tempo demais em um levantamento de requisitos completo e em um projeto detalhado (como ocorre no caso de um edifício residencial). Em vez disso, com software, é mais importante começar, até para entender, na prática, o problema e as necessidades dos clientes. Como consta de um dos doze princípios do Manifesto Ágil, que usamos na abertura deste capítulo, não devemos ter medo de mudanças, mas sim tirar proveito delas para gerar valor para os nossos clientes.
Porém, mesmo com processos ágeis, não é possível mudar de ideia a qualquer momento. Nenhum time de desenvolvimento consegue trabalhar se hoje a prioridade é uma funcionalidade X, mas amanhã ela muda para uma funcionalidade Y. Por isso mesmo, sprints devem possuir um objetivo claro, normalmente implementar um conjunto de histórias de usuários. Com isso, mudanças e adaptações podem acontecer, mas apenas entre sprints, como ilustrado na próxima figura. Durante um sprint, os times devem estar blindados contra mudanças. Por exemplo, o Guia do Scrum afirma categoricamente que durante um sprint nenhuma mudança pode comprometer seu objetivo.
Outra novidade importante que veio com métodos ágeis diz respeito à organização dos times de desenvolvimento e manutenção. Anteriormente, era comum ter times somente de desenvolvimento e times somente de manutenção (também chamados, às vezes, de times de sustentação). Ou seja, um time implementava e outro time mantinha. Essa dicotomia, no entanto, gera um problema grave: ela impede que o próprio time tenha que lidar com as consequências de um desenvolvimento mal feito. Ou seja, o time de desenvolvimento pode não se preocupar tanto com questões de código limpo, comentários, código flexível a mudanças, logging, dívida técnica, etc., tal como vimos nos capítulos anteriores. O motivo é que ele vai desenvolver, mas depois outro time será responsável pela manutenção.
Por isso, atualmente, o modelo mais comum de organização de times é
baseado em squads. Uma das principais características
desses times é sua responsabilidade fim a fim sobre o produto, desde a
concepção, passando pela implementação, bem como pela sua manutenção e
evolução. Com isso, favorece-se um estilo de trabalho que põe a
própria pele em risco
(skin in the game). Por exemplo, se o
time criar muita dívida técnica, depois caberá também a ele pagar essa
dívida.
9.2 Práticas para Manutenção de Software 🔗
Nesta seção, vamos comentar sobre práticas que podem ajudar na manutenção de software, incluindo revisão de código, slacks, post mortems, congelamento do código (code freeze) e recompensas por descoberta de bugs (bug bounties).
Outras práticas, mais tradicionais, também desempenham um papel importante em atividades de manutenção, como os quatro eventos típicos de Scrum (Planejamento, Reuniões Diárias, Revisão e Retrospectiva). No entanto, não vamos descrever tais eventos, pois eles já foram abordados no nosso livro anterior (Engenharia de Software Moderna, link).
9.2.1 Revisão de Código 🔗
Revisão de código é uma das práticas mais importantes para garantir a saúde a médio e longo prazo da base de código de um sistema. Ela é adotada por várias empresas que desenvolvem software. Por exemplo, o Stack Overflow Survey de 2019 incluiu uma pergunta sobre a adoção de revisão de código. Dentre os mais de 70 mil desenvolvedores que responderam a essa pergunta, apenas 23% não usavam revisão de código no seu trabalho.
A ideia de revisão de código é a seguinte: todo código desenvolvido por um desenvolvedor tem que ser, em seguida, analisado por pelo menos um outro desenvolvedor, chamado de revisor. O revisor pode adicionar comentários no código sob revisão, procurando esclarecer dúvidas, sugerindo melhorias, indicando bugs, etc.
Assim, estabelece-se um diálogo — na forma de uma troca de comentários — entre o autor do código e o seu revisor. Como resultado, o autor pode alterar a sua implementação para atender a alguma sugestão do revisor que ele achou procedente. Ou então, ele pode replicar e justificar que ela não faz sentido. Espera-se que após esse diálogo o código seja aprovado pelo revisor e possa ser integrado ao repositório do projeto. No entanto, o revisor pode recomendar também a rejeição definitiva da implementação proposta.
Normalmente, revisão de código é uma das funcionalidades oferecidas por plataformas de gerenciamento de código. Por exemplo, no GitHub, temos o conceito de Pull Requests (PRs), que permite que desenvolvedores submetam código para ser revisado e integrado no repositório principal de um projeto.
Literatura Científica: Em 2013, Alberto Bacchelli e Christian Bird realizaram um estudo sobre revisão de código com 873 desenvolvedores e testadores da Microsoft (link). As principais motivações para a realização de revisões de código, na opinião dos participantes desse estudo, foram as seguintes:
- Encontrar bugs no código (683 respostas)
- Melhorar o código (680 respostas)
- Sugerir implementações alternativas (501 respostas)
- Transferir conhecimento (333 respostas)
O último benefício acima pode ocorrer nos dois sentidos, isto é, o autor pode aprender com os comentários do revisor e vice-versa. Na verdade, revisão de código tem um efeito colateral importante que é evitar a formação de ilhas de conhecimento em um projeto. Em vez disso, ela ajuda a socializar o conhecimento sobre o código e a criar uma cultura de colaboração e troca de ideias entre os membros do time.
O que Revisar? Segue uma lista de questões que devem ser consideradas em revisões de código (indicamos também o capítulo no qual tratamos de cada item; quando o tratamento ocorreu no nosso livro anterior, Engenharia de Software Moderna, usamos a sigla ESM):
- Bugs em geral (Capítulo 5)
- Código mais complexo do que o necessário
- Código que usa um algoritmo ou estrutura de dados menos eficiente
- Código que viola princípios de projeto (Capítulo 5 de ESM)
- Código que viola a arquitetura do sistema (Capítulo 7 de ESM)
- Código que não trata exceções e erros (Capítulos 2 e 5)
- Código com code smells (Capítulo 9 de ESM)
- Otimizações prematuras (Capítulo 4 e Capítulo 9 de ESM)
- Ausência de testes (Capítulo 8 de ESM)
- Ausência de documentação (Capítulo 3)
- Falhas de segurança ou privacidade (Capítulo 5)
- Problemas de desempenho (Capítulo 5)
- Problemas de usabilidade (Capítulo 5)
- Uso inadequado ou subótimo de APIs
- Uso de bibliotecas ou frameworks que não são homologados pela organização
- Problemas com alocação de memória dinâmica (Capítulo 5)
- Problemas com programação concorrente (Capítulo 5)
- Código com problemas de organização ou indentação (Capítulo 2)
- Código que viola convenções de nome (Capítulo 2)
Recomendações para Revisores de Código: De forma geral, revisores devem focar em identificar problemas claros no código analisado, evitando sugestões que não possuem benefícios evidentes. Também é importante evitar comentários subjetivos ou baseados em preferências pessoais, como comentários sobre nomes de variáveis. A comunicação deve ser sempre educada e profissional, restrita ao código analisado, sem envolver questões pessoais.
Além disso, é importante justificar todas as sugestões,
principalmente ao interagir com desenvolvedores menos experientes.
Deve-se também adotar uma linguagem colaborativa, usando expressões como
nós
ou a gente
. Por fim, em situações de forte divergência
ou quando a discussão não evoluir de forma satisfatória, pode-se
recorrer a uma conversa síncrona para buscar consenso, embora isso deva
ser exceção.
Recomendações para Autores de PRs: Os autores também devem ser profissionais e educados em suas respostas. Eles devem entender que revisão de código não é um exame de proficiência. Ou seja, como autor, não leve a revisão para o lado pessoal e nunca imagine que o revisor está julgando a sua competência.
Outro ponto importante é que os autores devem submeter PRs pequenos, caso queiram obter uma resposta rápida e mais proveitosa dos revisores. Por exemplo, os autores do livro Software Engineering at Google recomendam que um PR deve ter no máximo 200 linhas de código.
Automatizando a Revisão: Antes de concluir, gostaríamos de comentar que diversos problemas tratados em uma revisão de código podem ser detectados de forma automática por meio de ferramentas de análise estática. Por exemplo, questões sobre convenções de nomes de identificadores (camelCase, snake_case, etc.), organização de código e estilo de indentação (tabs ou espaços, por exemplo) podem ser padronizadas por meio de linters. Com isso, evita-se que um revisor tenha que perder tempo com tais questões. Com os avanços de Modelos de Linguagens, diversas soluções baseadas em IA estão sendo propostas para automatizar atividades de revisão de código.
9.2.2 Slacks 🔗
Desenvolvimento de software, como qualquer projeto de engenharia, está sujeito a incertezas e riscos que são difíceis de prever inicialmente. Por isso, métodos como XP recomendam incluir uma folga (slack) em cada sprint. Por exemplo, pode-se reservar 20% da duração de cada sprint para realizar atividades como as seguintes:
Terminar a implementação de uma história de usuário que se revelou mais difícil de codificar; ou corrigir bugs que apareceram ao longo do sprint.
Melhorar a qualidade da implementação, por exemplo, refatorar parte do código, deixar o código mais limpo, corrigir problemas arquiteturais, melhorar a documentação, pagar dívida técnica, atualizar dependências, etc.
Estudar e testar uma nova tecnologia que pode trazer ganhos para o projeto, incluindo a realização de protótipos, provas de conceito ou MVPs.
Uma alternativa a slacks é reservar um dia a cada mês (ou a cada n meses) para a realização das duas últimas atividades descritas acima. Nesses dias, normalmente chamados de Hack Days, todos os membros do time focam em uma pauta de trabalho que não visa a implementação de novas funcionalidades.
Slacks ou Hack Days são então práticas que uma organização pode adotar para sinalizar a importância de cuidar da qualidade interna de seus produtos de software. Para isso, a organização entende ser importante reservar um tempo exclusivamente para essa finalidade.
Mundo Real: Shape Up é um processo de desenvolvimento de software proposto pela empresa 37signals, que desenvolve um sistema de gerenciamento de projetos chamado Basecamp. Shape Up possui duas trilhas de trabalho: uma trilha de descoberta de requisitos (discovery) e uma trilha de entrega (delivery) e implementação. A trilha de entrega é organizada em ciclos, que possuem tipicamente seis semanas. No entanto, quando um ciclo termina, não se começa imediatamente um novo ciclo. Em vez disso, ele é seguido de um período de cool-down, que dura duas semanas. Segundo os autores do processo Shape Up, o objetivo desse período é o seguinte:
Durante o período de cool-down, programadores e designers ficam livres para trabalhar no que quiserem. Após se dedicarem intensamente para entregar seus projetos de seis semanas, eles apreciam ter um tempo sob seu próprio controle. Eles usam esse tempo para corrigir bugs, explorar novas ideias ou experimentar novas possibilidades técnicas. (Shape Up: Stop Running in Circles and Ship Work that Matters, Capítulo 8)
Portanto, o período de cool-down é também uma forma de slack, na verdade, com uma duração até maior do que aquela proposta em XP.
9.2.3 Post Mortems 🔗
Post mortem é um documento que descreve, de forma detalhada, um
incidente grave que impactou as operações e serviços de uma organização.
No contexto específico de Engenharia de Software, esse incidente está
então relacionado com falhas de software. No entanto, também é possível
elaborar um post mortem após a morte
de um projeto, incluindo
projetos de desenvolvimento de software.
Em um post mortem, descrevem-se as causas do incidente (ou do cancelamento do projeto), o que funcionou bem e, principalmente, como o problema pode ser evitado no futuro, isto é, quais lições foram aprendidas. Portanto, o objetivo não é apontar culpados, mas sim evitar que os mesmos erros se repitam no futuro.
Normalmente, um post mortem possui as seguintes seções: linha do tempo (que descreve os acontecimentos relacionados ao incidente, incluindo data e hora); impacto do incidente; causas raiz; mitigação (isto é, como o problema foi resolvido); e as providências que serão tomadas para evitar que o incidente se repita.
Mundo Real: No dia 18 de novembro de 2025, um incidente grave ocorreu com a Cloudflare, um importante provedor de infraestrutura e serviços básicos de Internet. Como resultado, diversos sites que são hospedados ou que usam serviços desse provedor ficaram fora do ar, como ChatGPT, X e Spotify. No Brasil, segundo informações da Agência Brasil, alguns sistemas do Gov.br também foram afetados. De forma interessante, no mesmo dia, a empresa publicou um post mortem detalhado no seu site, explicando o incidente. Inclusive, esse post mortem foi assinado pelo co-fundador da empresa. A seguir, mostramos um resumo desse post mortem:
Linha do tempo. Em 18 de novembro de 2025, por volta de 11:20 UTC, a rede da Cloudflare começou a apresentar aumento de erros HTTP 500. Após investigação, os engenheiros identificaram que o problema estava relacionado a um arquivo de configuração usado pelos serviços da empresa. Por volta de 14:30 UTC, esse arquivo foi manualmente substituído por um arquivo que não apresentava problemas. Após uma fase de recuperação e reinicialização dos serviços, a Cloudflare voltou a operar de forma normal às 17:06.
Impacto. Durante o incidente, diversos sites que utilizam a infraestrutura da Cloudflare ficaram indisponíveis.
Causa raiz. Uma mudança nas permissões de um banco de dados levou à geração de entradas duplicadas em um arquivo de configuração de recursos usado pelo sistema de gerenciamento de bots. Esse arquivo tornou-se grande demais para ser processado, o que passou a gerar erros HTTP 500.
Mitigação. A equipe manualmente restaurou uma versão funcional do arquivo defeituoso.
Lições aprendidas: O post mortem também afirma o seguinte:
agora que nossos sistemas estão on-line e funcionando normalmente, começamos a trabalhar para fortalecê-los contra falhas como esta no futuro.
E, em seguida, detalham-se quatro providências que serão tomadas.
9.2.4 Congelamento do Código 🔗
Congelamento de código (code freeze ou, melhor ainda, code deployment freeze) é uma prática que proíbe mudanças na versão em produção de um sistema por um certo período de tempo. Especificamente, existem dois tipos de congelamento de código:
Congelamento de código total, quando nenhum código novo é colocado em produção, exceto correções de bugs críticos.
Congelamento de código parcial, quando novas funcionalidades não podem ser colocadas em produção, mas são possíveis correções de bugs simples e pequenas refatorações.
Essa prática costuma ser adotada por grandes empresas de software antes de períodos com grandes volumes de transações, como Black Friday e Natal. Pode ser usada também quando a maior parte da equipe está de férias, como no final do ano. Por fim, também pode ser útil quando, após uma implantação, um sistema passa a apresentar muitas instabilidades, sendo prudente suspender a implantação de novas funcionalidades até que a situação seja estabilizada.
9.2.5 Recompensas por Descobertas de Bugs 🔗
Programas de recompensas pela descoberta de bugs (bug bounties) são usados por grandes empresas de tecnologia para recompensar aqueles que identificarem bugs críticos, principalmente de segurança ou privacidade, em seus sistemas. Normalmente, esses programas são abertos a pesquisadores e desenvolvedores externos, algumas vezes chamados de hackers éticos. Quando encontram um problema, os participantes do programa devem reportá-lo de forma responsável, isto é, comunicando a falha diretamente para a empresa antes de qualquer divulgação pública. As recompensas variam de acordo com a gravidade do bug e podem chegar a valores bastante elevados em casos críticos. Em geral, as empresas definem claramente o escopo do programa, especificando quais sistemas podem ser testados e quais tipos de testes são permitidos.
9.3 Métricas 🔗
Tom DeMarco — um conhecido engenheiro e consultor de software —
possui uma frase clássica sobre métricas: Você não pode controlar
aquilo que não consegue medir.
De fato, não conseguimos controlar e
também não conseguimos entender e melhorar um processo de software
quando as decisões são subjetivas, isto é, não são baseadas em dados.
Por isso, é importante coletar métricas, mesmo que de forma mínima,
sobre diversos aspectos de um projeto de desenvolvimento e manutenção de
software. O objetivo é entender os problemas que estão acontecendo,
digamos assim, no chão da fábrica
, de forma que possamos atuar
para eliminá-los ou pelo menos atenuar os seus impactos negativos. Nesta
seção, vamos abordar quatro categorias importantes de métricas: métricas
de fluxo, métricas de qualidade, métricas de código fonte e métricas de
negócio.
9.3.1 Métricas de Fluxo 🔗
Essas métricas têm como objetivo acompanhar, de forma quantitativa, o fluxo de trabalho dos times de desenvolvimento e manutenção de software. Especificamente, podemos analisar o ritmo de implementação de novas funcionalidades por meio de duas métricas principais:
- Lead Time: tempo entre o recebimento de uma história de usuário por um time e sua disponibilização em produção. Em outras palavras, essa métrica mede quanto tempo uma história leva para ser entendida, implementada, revisada, testada e disponibilizada para os usuários do sistema. Para facilitar sua análise, normalmente consideramos a média do lead time das histórias implantadas durante um certo intervalo de tempo. Um lead time alto indica que os times estão demorando muito tempo para entregar valor para os usuários de um sistema.
- Throughput: número de histórias de usuário entregues em produção por unidade de tempo. Portanto, o throughput mede a vazão do trabalho realizado por um time.
Essas duas métricas capturam dimensões complementares de um processo de desenvolvimento e evolução de software. O lead time mede quanto tempo um cliente precisa esperar para usar uma funcionalidade, enquanto o throughput mede quantas funcionalidades a equipe consegue entregar em um determinado período.
Para entender melhor as métricas, podemos fazer uma analogia com um restaurante. Um restaurante não pode ter um lead time alto, pois os clientes não querem esperar horas para serem servidos. Porém, também não adianta atender a um único cliente de forma rápida enquanto vários outros permanecem esperando. O objetivo é reduzir o tempo de espera dos clientes (lead time) e, ao mesmo tempo, manter uma boa vazão de atendimento (throughput). Nessa analogia, o restaurante são os times de desenvolvimento e os pratos correspondem às histórias de usuário que esses times devem implementar.
Existe uma lei em Teoria de Filas que permite calcular o número médio de histórias de usuário que estão sob a responsabilidade de um time. Esse valor é chamado de Work in Progress (WIP). A lei, chamada Lei de Little, define o seguinte:
WIP = Throughput x Lead Time
Ou seja, conhecendo o throughput e o lead time médios de um time, medidos durante um certo período, conseguimos calcular o número médio de histórias que estiveram sob a sua responsabilidade durante esse período (WIP). Voltando à analogia do restaurante, o WIP seria o número médio de pratos que a cozinha do restaurante ficou preparando durante o período da análise.
Por exemplo, suponha que um time entregue em média 3 histórias por semana (throughput) e que as histórias levam em média duas semanas para ficarem prontas (lead time). Logo, esse time, em média, está sempre com seis histórias sob sua responsabilidade (WIP).
Para calcular o lead time e o throughput é importante que exista uma baixa variabilidade no tamanho das histórias de usuário, pois ambas as métricas são calculadas como sendo uma média. Se as histórias tiverem tamanhos muito diferentes, podemos unir histórias pequenas ou quebrar aquelas que são muito grandes. Uma outra alternativa é calcular o lead time e throughput para classes de histórias. Por exemplo, para histórias pequenas, médias e grandes.
Aprofundamento: Existem outras interpretações do conceito de lead time, além daquela apresentada anteriormente. Por exemplo, o lead time do cliente é o tempo entre o recebimento de uma solicitação do cliente e sua entrega. Essa definição de lead time é mais adequada quando um sistema é desenvolvido de forma customizada para um cliente específico. Por outro lado, esse não é o caso de sistemas desenvolvidos para um mercado formado por milhares ou milhões de clientes (quando é natural que uma solicitação tenha que ser entendida, validada e priorizada, antes de sua implementação ser alocada para um time).
9.3.2 Métricas de Qualidade 🔗
Apesar de as métricas de fluxo serem importantes, não basta entregar novas histórias de usuário com velocidade e com vazão. Também precisamos garantir que essas entregas tenham qualidade. Para isso, é importante considerar métricas como as seguintes:
Número de bugs em produção, possivelmente, por unidade de tempo. Por exemplo, essa é a principal métrica de qualidade defendida por XP, que advoga que
nenhum defeito é aceitável; porém, cada defeito é também uma oportunidade para os times aprenderem e melhorarem
. Se o sistema estiver em uma fase de forte crescimento, pode ser útil normalizar essa métrica, isto é, medir o número de bugs em produção por KLOC (milhares de linhas de código).Tempo médio para correção de bugs. O motivo é que não adianta ter poucos bugs, mas eles demorarem para serem corrigidos. Assim, queremos poucos bugs em produção e que eles sejam rapidamente corrigidos.
Taxa de reabertura de bugs. Essa é outra métrica importante, pois também não é suficiente corrigir os bugs rapidamente. É importante que essa correção seja efetiva e que resolva o problema. Isto é, o percentual de retrabalho devido à reabertura de bugs deve ser mínimo. Para diminuir a taxa de reabertura, é importante investir também no processo de triagem de bugs. O motivo é que a triagem pode decidir que um bug não existe, mas depois percebe-se que ele é, de fato, verdadeiro. Esse caso conta como uma reabertura, pois o bug foi fechado como inexistente, mas depois se constatou que isso foi uma decisão equivocada.
9.3.3 Métricas de Código Fonte 🔗
É interessante acompanhar também propriedades do código fonte, como
tamanho e complexidade. No caso de tamanho, podemos usar métricas como
número de linhas de código (LOC), número de classes, número de pacotes,
etc. No caso de complexidade, uma métrica muito comum é a
complexidade ciclomática (CC). Essa métrica mede a
complexidade de uma função com base no número de caminhos possíveis de
execução. Na prática, ela pode ser calculada somando 1 ao número de
comandos de decisão no código (como if, while,
for e case). Quanto maior o valor de CC, mais
difícil tende a ser entender, manter e testar a função. Por exemplo,
valores de CC acima de 10 indicam funções excessivamente complexas.
9.3.4 Métricas de Negócio 🔗
No contexto de desenvolvimento e manutenção modernos de software é importante que os times consigam, proativamente, contribuir com os resultados de uma organização. Por isso, é importante que acompanhem também métricas de negócio. No caso de software por assinatura (Software as a Service ou SaaS), as métricas mais comuns são:
- Custo de Aquisição de Clientes (CAC) (Customer Acquisition Cost), que mede os custos mensais com propaganda, vendas, eventos, dentre outros, dividido pelo número de novos assinantes no mês.
- Receita Mensal Recorrente (MRR) (Monthly Recurring Revenue): somatório dos valores das assinaturas recebidas no mês. Ou seja, é o faturamento mensal obtido com o sistema.
- Taxa de Cancelamentos (Churn Rate): número de cancelamentos no mês dividido pelo total de clientes no início do mês. É importante entender que todo sistema por assinatura terá uma taxa de cancelamento. Por exemplo, taxas de cancelamento abaixo de 5% já são consideradas boas. Por outro lado, valores acima de 10% tendem a acender um alerta vermelho.
- Valor do Tempo de Vida do Cliente (LTV) (Customer Lifetime Value): assinatura média no mês dividida pela taxa de cancelamentos. Exemplo: se a assinatura média é de R$ 100 e a taxa de cancelamento for de 5%, temos que LTV = 100 / 0.05 = R$ 2.000. Ou seja, um cliente em média gera R$ 2.000 de receita antes de cancelar o serviço.
Para um SaaS ser viável, LTV > CAC. Na verdade, recomenda-se que LTV / CAC > 3.
9.3.5 Recomendações para Uso de Métricas 🔗
Para encerrar esta seção, seguem algumas recomendações para uso de métricas:
1. Métricas são indicadores para ajudar no entendimento e diagnóstico de um problema. Elas nunca podem ser o objetivo final. Apenas para ilustrar, se a produtividade de desenvolvedores for medida em linhas de código ou número de commits, eles serão incentivados a gerar código redundante, duplicado, que não agrega valor, etc.
2. Deve-se sempre entender a distribuição estatística que descreve os valores das métricas. Por exemplo, métricas de código fonte podem ter uma distribuição no formato de uma cauda longa, o que compromete o uso de valores médios. Para ilustrar, a maioria das funções de um sistema podem ser pequenas, mas podem existir também algumas funções muito grandes, que vão distorcer o tamanho médio da população de funções.
3. Além de entender a distribuição dos valores de uma métrica, devemos entender a evolução desses valores ao longo do tempo. Por exemplo, uma sazonalidade pode aumentar o número de bugs de um sistema em um certo mês. No entanto, mais importante do que esse aumento em um único mês, devemos acompanhar a evolução da métrica ao longo de mais meses, para não tomar decisões precipitadas.
4. Não devemos usar uma única métrica. Por exemplo, no caso das métricas de processo, recomendamos anteriormente usar tanto métricas de fluxo como de qualidade. Assim, se os desenvolvedores somente privilegiarem a velocidade de entregas, eles poderão ser penalizados nas métricas de qualidade. Por outro lado, se eles ficarem focados em alcançar níveis de qualidade que não são necessários, eles serão penalizados pelas métricas de fluxo.
5. Por fim, não devemos usar um número excessivo de métricas, pois podemos ficar perdidos ao tentar entender uma grande quantidade de dados. Além disso, é comum ter correlações entre métricas. Por exemplo, tende a existir uma correlação alta entre o tamanho de classes e o número de métodos por classe. Logo, basta considerar uma dessas duas métricas.
9.4 Modelos de Branches 🔗
Já caminhando para o final do capítulo, vamos apresentar três modelos que padronizam os branches que devem ser criados por times de desenvolvimento e manutenção de software. São eles: Git-flow, GitHub Flow e Desenvolvimento Baseado no Trunk (TBD). Esse é um assunto mais técnico e operacional, por isso o deixamos para o final do capítulo. Porém, a escolha do modelo de branches é muito relevante em processos de manutenção de software, pois eles disciplinam o fluxo de trabalho usado pelos times para implementar novas funcionalidades ou para corrigir bugs, por exemplo.
9.4.1 Git-flow 🔗
Git-flow é um modelo de gerenciamento de branches muito comum em times que usam Git, tendo sido proposto por Vincent Driessen em 2010. Basicamente, o modelo usa dois branches principais e permanentes:
Main, também conhecido comomasteroutrunk, é usado para armazenar as versões de um sistema que estão em produção.Developé usado para armazenar código com funcionalidades que já foram implementadas, mas que ainda não passaram por um teste final. Normalmente, esse teste é realizado por um analista de qualidade (QA).
Git-flow prevê ainda três branches temporários: branches de funcionalidade, branches de release e branches de hotfix. Esses branches serão descritos a seguir. Eles podem ser criados usando os próprios comandos do Git ou então usando plugins com macros que facilitam o uso do Git-flow.
Branches de Funcionalidade: Esses branches nascem de
develop e devem ser criados antes de começar a
implementação de uma nova funcionalidade. Quando essa funcionalidade
ficar pronta, eles são integrados (via um merge) de volta
em develop e são removidos. Por isso, eles existem apenas
no repositório local de um desenvolvedor.
Na próxima figura, mostramos um branch de funcionalidade. Os círculos
cheios representam commits e o círculo vazio representa um
merge. Veja que o branch de funcionalidade nasce e volta
para develop.
Branches de Release: Esses branches também nascem de
develop. Eles são usados para preparar uma nova release, a
qual deve ser aprovada (ou homologada) pelo cliente final. Quando o
cliente dá o sinal verde, os branches de release são integrados no
main, pois agora temos uma nova versão do sistema pronta
para entrar em produção. Porém, se no processo de aprovação forem
realizadas mudanças no código, o branch de release deve ser integrado de
volta também em develop.
Para exemplificar, suponha que após implementar a funcionalidade da
figura anterior, o líder do time decidiu gerar uma release 1.0 do
sistema. Para isso, ele criou um branch de release (veja a próxima
figura), que foi usado para mostrar o sistema para o seu cliente final.
Após algumas modificações requisitadas pelo cliente, o sistema foi
aprovado e entrou em produção, isto é, foi integrado no
main, usando-se a tag 1.0. Por fim, as mudanças realizadas
no branch de release foram também aplicadas em develop.
Branches de Hotfix: Esses branches são usados para
corrigir um bug crítico que foi detectado em produção, ou seja, em um
código que está no main. Por isso, eles nascem do
main, recebem commits que corrigem o bug crítico e, por
último, são novamente integrados no main e também em
develop.
Continuando com nosso exemplo, suponha que após lançar a nova
release, os usuários reportaram um bug crítico nela. Assim, criou-se um
branch para corrigir esse bug crítico (veja na figura a seguir), o qual
nasceu do main. Após a correção do bug, o branch foi
integrado de volta no main e uma nova release foi gerada,
com a tag 1.0.1. Por fim, o branch foi também integrado em
develop.
Em resumo, o fluxo mais comum quando se usa Git-flow é o seguinte:
Funcionalidade ⇒ develop ⇒ release ⇒ main
Ou seja, uma funcionalidade é sempre implementada em um branch
específico. Em seguida, esse branch é integrado em develop,
onde a funcionalidade passa por testes de integração. De tempos em
tempos, um branch de release é gerado para mostrar uma nova versão do
sistema para o cliente final. Uma vez aprovada, essa versão é integrada
no main e disponibilizada para a base completa de
usuários.
Git-flow deve ser usado, principalmente, quando existem testes manuais e times de garantia de qualidade (QA). E também quando os clientes precisam aprovar e homologar qualquer nova versão do sistema antes que ela entre em produção.
Porém, quando se usa Git-flow, os branches de funcionalidade podem
demorar muito tempo para serem integrados em develop, o que
pode ocasionar diversos conflitos de integração
(merge hell). Adicionalmente, caso a integração dos branches de
release também demore, os desenvolvedores vão demorar para receber
feedback sobre as novas funcionalidades que eles implementaram.
9.4.2 GitHub Flow 🔗
GitHub Flow é um modelo de branches muito comum quando se usa GitHub. Trata-se de um modelo mais simples do que Git-flow, pois existe apenas um branch principal e branches de funcionalidade. Por outro lado, existe suporte a revisão de código antes de integração no branch principal, por meio do mecanismo de Pull Requests (PR) do GitHub.
Quando se usa GitHub Flow, os principais passos são os seguintes:
- Um desenvolvedor cria um branch no seu repositório local.
- Implementa uma funcionalidade ou corrige um bug.
- Faz um push do branch para o GitHub.
- Entra no GitHub e abre um Pull Request, isto é, um pedido para alguém revisar seu branch, conforme descrevemos na Seção 9.2.1.
- Um outro desenvolvedor revisa e, eventualmente, faz o merge do PR.
GitHub Flow é usado, principalmente, em sistemas com apenas uma versão em produção, como costuma ser o caso de sistemas Web. Uma desvantagem do modelo é que os PRs podem levar muito tempo para serem revisados.
Apesar do nome, os mesmos passos propostos por GitHub Flow podem ser usados com outros serviços de controle de versão, como GitLab.
9.4.3 Desenvolvimento Baseado no Trunk 🔗
Desenvolvimento Baseado no Trunk (TBD) é ainda mais simples do que
GitHub Flow, pois baseia-se em um único branch, isto é, o branch
principal, chamado também de main, master ou
trunk. Ou seja, todos os desenvolvedores realizam seus
commits diretamente no branch principal. No entanto e apenas para
esclarecer, mesmo em TBD, desenvolvedores podem criar branches de
funcionalidade, mas eles devem ter uma duração muito limitada, por
exemplo, de no máximo alguns dias.
Para usar TBD, é importante que exista uma boa quantidade de testes
de unidade e de integração, para evitar que bugs e regressões sejam
introduzidos no main. No nosso livro anterior (Engenharia
de Software Moderna, link) explicamos
também o mecanismo de feature flags, que é usado para
evitar que implementações incompletas entrem em produção.
9.5 Conventional Commits 🔗
Manutenção, em suas várias formas (corretiva, evolutiva, refatoração, etc.), requer essencialmente mudanças de código, as quais são efetivadas por meio de commits. Nesta seção, vamos descrever uma convenção para escrita de mensagens de commits, chamada de Conventional Commits. Essas mensagens são importantes porque elas são parte do histórico de versões mantido por um repositório de código e, futuramente, serão lidas por outros desenvolvedores.
Antes de apresentar essa convenção, queremos lembrar que todo
git commit possui uma mensagem que explica a mudança que
está sendo realizada no repositório do projeto, tal como a seguir:
git commit -m "update"
No entanto, a mensagem acima, que usamos propositalmente como exemplo, é muito simples e vaga. Conventional Commits propõe então um formato padrão para escrita de mensagens de commits, com o objetivo de torná-las mais legíveis e também facilitar o seu processamento por ferramentas. Cada mensagem deve ter o seguinte formato:
<tipo>(escopo): <descrição>
[corpo]
[rodapé]
O campo <tipo> indica o tipo da mudança e pode
ser:
feat: implementação de uma funcionalidade (logo, manutenção evolutiva).fix: correção de um bug (logo, manutenção corretiva).refactor: refatoração.docs: mudança em documentação (por exemplo, no README do projeto).
Já <escopo> é um campo opcional que informa o
módulo ou a parte do sistema que foi alvo da mudança. Por exemplo,
(controlador).
Em seguida, <descrição> é uma breve descrição da
mudança, em apenas uma linha. A descrição deve começar com um verbo no
presente. Por exemplo, cria, atualiza,
refatora, corrige, implementa,
remove, envia, etc.
Opcionalmente, deve seguir uma linha em branco e uma série de linhas
(<corpo>) que descrevem a mudança com mais detalhes.
O <corpo> é opcional porque a mudança pode ser
simples e a <descrição> já pode ser suficiente para
explicá-la.
O [rodapé] é também opcional. Um rodapé comum é a
mensagem BREAKING CHANGE:, a qual destaca que o commit
inclui uma breaking change, tal como vimos no Capítulo 4. No entanto, em
vez de indicar a breaking change no rodapé, pode-se acrescentar um ponto
de exclamação (!) após o <tipo>. Por
exemplo, feat! significa que estamos implementando uma
funcionalidade e que ela introduz uma breaking change. O rodapé pode ser
também usado para associar o commit a uma tarefa no sistema de
gerenciamento de tarefas, como Jira, GitHub Issues, etc.
Uma implicação importante de Conventional Commits é que um commit deve realizar apenas um tipo de manutenção. Ou seja, não é possível ter um commit que corrige um bug e que implementa uma funcionalidade, por exemplo. Nesse caso, esse commit deve ser dividido em dois. De forma semelhante, não é recomendável ter um commit que implementa múltiplas vezes o mesmo tipo de manutenção. Por exemplo, o mesmo commit não deve corrigir dois bugs.
9.5.1 Exemplo: Fórum de Perguntas e Respostas 🔗
Mostram-se a seguir alguns exemplos de mensagens de commits, relativas a um fórum de perguntas e respostas. Ou seja, um sistema no qual um usuário pode fazer perguntas, que serão respondidas por outros usuários.
1. Implementação de uma nova feature.
feat(perguntas): adiciona um botão de like nas perguntas
2. Correção de um bug.
fix(respostas): corrige bug na contagem de visualizações de respostas
3. Atualização de documentação.
docs: atualiza instruções de instalação
4. Implementação de uma feature, com uma breaking change.
feat(respostas)!: cria uma thread de respostas
Permite agora adicionar uma resposta em uma outra resposta,
criando-se assim uma thread de respostas.
BREAKING CHANGE: adição de parâmetro com o nível da resposta
no endpoint de criação de resposta.
9.5.2 Exemplo: Angular 🔗
Um dos primeiros sistemas a adotar Conventional Commits foi o
Angular, que é um framework para implementação de frontends de
aplicações Web. Seguem alguns exemplos de mensagens de commits desse
projeto. O leitor vai perceber que Conventional Commits também permite
outros tipos de mudanças, como test (implementação de
testes) e build (mudanças no sistema de build).
docs: fix wrong line highlights
fix(language-service): Detect local project version on creation
feat(forms): introduce parse errors in signal forms
refactor(core): improve tree-shaking
feat(devtools): mark special element injector providers as internal
test(forms): submit behavior while validation is pending
build: update cross-repo angular dependencies
9.5.3 Aplicações 🔗
Como afirmado, Conventional Commits padroniza e torna mais claras as mensagens de commit, facilitando o entendimento das manutenções realizadas em um projeto. Ele também facilita a implementação de ferramentas para geração de changelogs, versionamento semântico e geração de notas de release.
Bibliografia 🔗
Marco Tulio Valente. Engenharia de Software Moderna: Princípios e Práticas para Desenvolvimento de Software com Produtividade. Editora Independente, 2020.
Titus Winters, Tom Manshreck, Hyrum Wright. Software Engineering at Google: Lessons Learned from Programming Over Time. O’Reilly, 2020.
Kent Beck, Cynthia Andres. Extreme Programming Explained: Embrace Change. Addison-Wesley, 2nd edition, 2004.
Vincent Driessen. A successful Git branching model. Blog, 2010.
Nicole Forsgren, Jez Humble, Gene Kim. Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations. It Revolution, 2018.
Exercícios 🔗
1. Qual é a principal diferença entre revisão de código e programação em pares (uma das principais práticas propostas por XP)?
2. Descreva uma desvantagem de revisão de código.
3. Qual é a relação entre Conventional Commits e Versionamento Semântico (tal como estudamos no Capítulo 4)?
4. A próxima tabela apresenta valores de métricas coletadas ao longo de quatro meses por um time de desenvolvimento e manutenção de software. Todas as métricas de fluxo estão expressas na mesma unidade de tempo (semanas).
| Mês | Histórias entregues por semana | Lead time médio (semanas) | Bugs em produção |
|---|---|---|---|
| Jan | 3,0 | 2 | 3 |
| Fev | 4,5 | 2 | 9 |
| Mar | 5,0 | 2 | 15 |
| Abr | 5,5 | 2 | 21 |
Usando a Lei de Little, calcule o WIP do time em cada um dos quatro meses. Qual é a tendência dos valores de WIP ao longo dos meses?
O time está comemorando o aumento de throughput como um sinal de eficiência. No entanto, qual efeito negativo pode estar ocorrendo em função desse aumento de throughput?
5. Um time de desenvolvimento e manutenção afirma que seu processo de correção de bugs é muito eficiente, pois ele sempre consegue corrigir todos os bugs reportados pelos usuários. No entanto, quais outras métricas deveriam ser analisadas antes de chegar a uma conclusão mais definitiva sobre a eficiência desse time? Justifique sua resposta.
6. Um conjunto conhecido de métricas para avaliar o desempenho de entrega de software é chamado de métricas DORA (DevOps Research and Assessment). Pesquise sobre as métricas DORA e descreva a diferença entre o conceito de lead time de tais métricas e aquele que definimos na Seção 9.3.1.
7. Define-se como churn negativo de receita (negative revenue churn) a situação em que o aumento de receita proveniente dos clientes que permanecem em um sistema supera a perda de receita causada pelo cancelamento de outros clientes. Para ficar mais claro, considere o seguinte exemplo: um serviço SaaS possui 100 clientes e fatura R$ 10 mil por mês. Em um determinado mês, 10 clientes cancelaram o serviço. Apesar disso, a empresa passou a faturar R$ 11 mil por mês com os 90 clientes restantes.
Como é possível que a receita total tenha aumentado mesmo com a perda de clientes? Justifique.
Cite pelo menos dois eventos que podem ter contribuído para essa situação.
8. Supondo uma organização que usa Git-flow, complete a próxima tabela com os nomes dos branches de origem e de destino para cada tipo de branch.
| Tipo de Branch | Branch de Origem | Branch de Destino |
|---|---|---|
| Feature | ||
| Release | ||
| Hotfix |