Testes e qualidade de código

[Code Smell #5] Shotgun Surgery vs. Divergent Change

Shotgun Surgery é um smell em que uma mudança se espalha por vários arquivos, enquanto Divergent Change é sua imagem espelhada: um arquivo muda por vários motivos. Este artigo explica como medir e distinguir esses dois smells opostos usando o histórico de commits.

6 min de leitura
Imagem de capa de [Code Smell #5] Shotgun Surgery vs. Divergent Change

“É só adicionar um método de pagamento. Por que está demorando tanto?”

Este artigo continua o anterior Code Smell #4.

É uma pergunta frequente da área de produto e também difícil de responder para desenvolvedores.

Porque, na prática, há doze pontos para alterar: adicionar um case ao enum, modificar cinco switch, a tabela de mapeamento de ícones, constantes de string, nomes de eventos de analytics, conversão de parâmetros do servidor e fixtures de teste.

Cada alteração tem duas ou três linhas e nada é difícil. O difícil é encontrar os doze pontos sem deixar nenhum de fora.

Martin Fowler dá um nome a essa situação em “Refactoring”: Shotgun Surgery (Original do autor).

Por que o nome é preciso

Quando uma espingarda dispara, os chumbos se espalham. O termo significa que uma mudança aparece como pequenas alterações espalhadas pelo código-base.

Cada ferida é superficial, mas são muitas, e deixar qualquer uma passar causa problemas.

O problema real aparece quando algo fica de fora. Se você corrige onze de doze pontos e faz o deploy, o restante pode aparecer apenas em uma condição específica.

A tela de pagamento funciona, mas a tela de recibo mostra “método de pagamento desconhecido”. O custo de Shotgun Surgery não é o tempo de alteração, mas a probabilidade de omissão.

Existe um smell espelhado

A mesma lista contém o smell oposto: Divergent Change (chamado de “Tangled Change” em algumas traduções).

  • Divergent Change: um módulo muda com frequência por vários motivos diferentes. Se o banco de dados muda, você altera este arquivo; se as regras de pagamento mudam, altera este arquivo novamente.
  • Shotgun Surgery: uma mudança por um único motivo afeta vários módulos (Artigo original sobre Shotgun Surgery).

Se você desenhar a relação entre os dois, os eixos estarão invertidos.

Divergent Change Shotgun Surgery
Responsabilidades e código Várias responsabilidades em um só lugar Uma responsabilidade em vários lugares
Sintoma Este arquivo muda continuamente Esta mudança continua se espalhando
Solução Separar Consolidar

O fato de as soluções serem opostas é importante. Discussões sobre code smells costumam chegar a “divida”, mas a resposta para Shotgun Surgery é consolidar.

Se você identificar o problema errado, a solução seguirá exatamente na direção contrária.

Há apenas um eixo que separa os dois: o motivo da mudança.

É por isso que o Princípio da Responsabilidade Única é definido não como “deve fazer apenas uma coisa”, mas como “deve ter um único motivo para mudar”. A quantidade de tarefas varia conforme quem conta, mas os motivos da mudança podem ser verificados no histórico real de commits.

Diagrama comparativo de code smells contrastando as direções de mudança de Divergent Change e Shotgun Surgery
Separe Divergent Change; consolide Shotgun Surgery

Medindo com o histórico de commits

Você pode usar dados em vez de intuição. Shotgun Surgery aparece como arquivos que sempre mudam juntos.

Isso é chamado de acoplamento de mudanças (change coupling) ou acoplamento lógico.

importO ponto central é que esse acoplamento não aparece nas relações. Se dois arquivos não fazem referência um ao outro, mas sempre aparecem no mesmo commit, uma regra não escrita no código está mantendo os dois unidos.

Conte os pares de arquivos alterados juntos nos commits recentes para encontrar candidatos.

git log --format='%H' --since=6.months.ago | while read c; do
  git show --format= --name-only "$c" | grep '\.swift$' | sort | \
    awk 'NR==FNR{a[NR]=$0;n=NR} END{for(i=1;i<n;i++)for(j=i+1;j<=n;j++)print a[i]" + "a[j]}'
done | sort | uniq -c | sort -rn | head -20

Depois, verifique se os pares no topo realmente deveriam pertencer ao mesmo código.

É claro que alguns pares mudam juntos naturalmente, como uma implementação e seu arquivo de teste.

O que restar depois da filtragem é o que deve ser investigado.

Como consolidar

Consolidar ramificações condicionais espalhadas em um único tipo é a forma mais comum.

// Antes: o conhecimento sobre métodos de pagamento switch está espalhado pelo projeto
func iconName(for method: PaymentMethod) -> String {
    switch method {
    case .card: return "creditcard"
    case .transfer: return "building.columns"
    }
}
func displayName(for method: PaymentMethod) -> String { ... }
func serverCode(for method: PaymentMethod) -> String { ... }

Se essas três funções estiverem em arquivos diferentes, cada novo método de pagamento exige procurar em três lugares. Ao consolidá-las em um tipo, passa a haver um único lugar.

// Depois: basta consultar um único lugar
extension PaymentMethod {
    var iconName: String { ... }
    var displayName: String { ... }
    var serverCode: String { ... }
}

Deixe o compilador detectar omissões. No Swift, switch precisa lidar com todos os casos.

Ao adicionar um case ao enum, todos os switch sem default geram um erro de compilação. Isso não elimina Shotgun Surgery, mas move as omissões do runtime para o tempo de compilação.

Como o custo real mencionado antes era a probabilidade de omissão, isso por si só muda a situação.

Por isso, não adicione habitualmente default: break aos switch que lidam com enums. É como cortar essa rede de segurança com as próprias mãos.

Transforme chaves de string em tipos. Se nomes de eventos de analytics, chaves de UserDefaults e nomes de notificações estiverem espalhados como literais de string, o compilador não poderá ajudar.

Reuni-los como constantes em um só lugar ou envolvê-los em um tipo cria um único ponto de adição e alteração.

Transforme a configuração em dados. Se cada método de pagamento precisa de ícone, nome e código, você pode defini-los em um único array de structs em vez de espalhá-los pelo código.

Adicionar um novo método passa a ser apenas incluir um item no array.

Se for difícil consolidar, deixe ao menos uma indicação. Há casos que não podem ser consolidados fisicamente.

Por exemplo, quando servidor e cliente precisam ter a mesma regra.

Nesse caso, adicione comentários apontando uns para os outros em cada ponto ou crie um teste que falhe quando os valores divergirem. Se for inevitável depender da memória humana, pelo menos registre onde lembrar.

Imagem conceitual de refatoração que reúne ramificações espalhadas em um único lugar
Ao reunir regras espalhadas em um único tipo, o ponto de adição passa a ser um só

Entre separar e consolidar

Aqui retomamos os artigos anteriores. As soluções para code smells geralmente seguem apenas duas direções, separar ou consolidar, mas exagerar em qualquer uma cria outro smell.

  • Separar para corrigir Divergent Change → código Ravioli
  • Consolidar para corrigir Shotgun Surgery → God Object
  • Separar em camadas → código Lasanha

Por isso, o objetivo não deve ser o tamanho ou a quantidade dos fragmentos. Há um único objetivo.

Mantenha juntas as coisas que mudam juntas e separadas as que mudam separadamente. É isso que coesão e acoplamento dizem, no fim das contas.

Quando houver dúvida, use esta pergunta: tente lembrar umas três vezes “por que alterei este código mais recentemente?”

Se o motivo foi diferente a cada vez, é hora de separar; se você alterou vários lugares sempre pelo mesmo motivo, é hora de consolidar.

Resumo

  • Shotgun Surgery é o estado em que uma única mudança se espalha como pequenas alterações em vários arquivos.
  • O custo real não é o tempo de alteração, mas a probabilidade de omissão.
  • Seu smell oposto, Divergent Change, é o estado em que um arquivo muda por vários motivos, e a solução também é oposta. Um deve ser consolidado e o outro, separado.
  • Extrair do histórico de commits os pares de arquivos que sempre mudam juntos permite medir os candidatos.
  • No Swift, a verificação exaustiva de enums e switch transforma omissões em erros de compilação. Não adicionar default por hábito é a forma de preservar essa rede de segurança.

O próximo artigo aborda a metáfora que fez todos esses smells receberem um único nome: dívida técnica.

O significado original dito por Ward Cunningham não era “código sujo”.

Fontes e critérios de verificação

  • Refactoring — Martin Fowler · Original do autor · Verificado em 2026-08-17 · Base: smells Shotgun Surgery e Divergent Change e princípios de refatoração
  • The Shotgun Surgery Problem — Martin Fowler · Original do autor · Verificado em 2026-08-17 · Base: exemplos de Shotgun Surgery em que mudanças se espalham por vários módulos

Continue lendo

Série de Code Smells