Como aplicar o padrão strategy para eliminar condicionais complexas

1. Entendendo o problema: condicionais complexas e seus custos

1.1. O que são condicionais complexas e por que elas surgem

Condicionais complexas são estruturas de decisão que crescem descontroladamente à medida que novos requisitos de negócio são adicionados. Elas surgem naturalmente quando desenvolvedores implementam lógicas de variação diretamente no fluxo principal do código, sem antecipar a necessidade de extensibilidade.

1.2. Problemas de manutenção, legibilidade e testabilidade

O acúmulo de condicionais traz consequências graves:

  • Manutenção: cada novo requisito exige alteração em código já testado
  • Legibilidade: métodos com dezenas de if/else ou switch tornam-se ilegíveis
  • Testabilidade: testar todos os caminhos condicionais exige cenários complexos e frágeis

1.3. Exemplo clássico: múltiplos if/else ou switch gigantes

Considere um sistema de cálculo de frete que precisa lidar com diferentes modalidades:

function calcularFrete(tipo, peso, distancia) {
  let valor;

  if (tipo === 'sedex') {
    valor = peso * 0.5 + distancia * 0.3;
    if (peso > 10) valor += 15;
  } else if (tipo === 'pac') {
    valor = peso * 0.3 + distancia * 0.2;
    if (distancia > 100) valor *= 0.9;
  } else if (tipo === 'expresso') {
    valor = peso * 0.8 + distancia * 0.5;
    valor += 10; // taxa de urgência
  } else if (tipo === 'internacional') {
    valor = peso * 1.2 + distancia * 0.8;
    valor *= 1.15; // taxa alfandegária
  } else {
    throw new Error('Tipo de frete inválido');
  }

  return valor;
}

Este código viola o Princípio Aberto/Fechado (Open/Closed Principle) e torna-se um pesadelo de manutenção.

2. Fundamentos do padrão strategy

2.1. Definição e princípios do padrão (Open/Closed Principle)

O padrão Strategy define uma família de algoritmos intercambiáveis, encapsulando cada um deles em uma classe separada. Ele segue o princípio de que o código deve estar aberto para extensão (novas estratégias) mas fechado para modificação (código existente permanece intacto).

2.2. Estrutura básica: Contexto, Estratégia e Estratégias Concretas

  • Contexto: classe que utiliza a estratégia, sem conhecer os detalhes de implementação
  • Estratégia: interface ou classe abstrata que define o contrato comum
  • Estratégias Concretas: implementações específicas de cada variação de comportamento

2.3. Diferenças entre strategy e outros padrões (state, template method)

  • State: altera comportamento baseado no estado interno do objeto
  • Template Method: define o esqueleto de um algoritmo, deixando subclasses implementarem partes
  • Strategy: foca em selecionar um algoritmo completo em tempo de execução

3. Mapeando condicionais para estratégias

3.1. Identificando variações de comportamento no código existente

Analise cada ramo condicional e identifique o que realmente varia: cálculo, validação, formatação, etc.

3.2. Criando uma interface de estratégia comum

// Interface comum para todas as estratégias de frete
interface FreteStrategy {
  calcular(peso: number, distancia: number): number;
}

3.3. Extraindo cada ramo condicional para uma estratégia concreta

Cada if/else ou case do switch vira uma classe separada que implementa a interface.

4. Implementação prática: substituindo um switch por estratégias

4.1. Código original com condicionais complexas (exemplo em TypeScript)

class CalculadoraFrete {
  calcular(tipo: string, peso: number, distancia: number): number {
    if (tipo === 'sedex') {
      let valor = peso * 0.5 + distancia * 0.3;
      if (peso > 10) valor += 15;
      return valor;
    } else if (tipo === 'pac') {
      let valor = peso * 0.3 + distancia * 0.2;
      if (distancia > 100) valor *= 0.9;
      return valor;
    } else if (tipo === 'expresso') {
      return peso * 0.8 + distancia * 0.5 + 10;
    }
    throw new Error('Tipo inválido');
  }
}

4.2. Refatoração passo a passo: interface, classes e contexto

Passo 1: Criar a interface de estratégia

interface FreteStrategy {
  calcular(peso: number, distancia: number): number;
}

Passo 2: Implementar cada estratégia concreta

class SedexStrategy implements FreteStrategy {
  calcular(peso: number, distancia: number): number {
    let valor = peso * 0.5 + distancia * 0.3;
    if (peso > 10) valor += 15;
    return valor;
  }
}

class PacStrategy implements FreteStrategy {
  calcular(peso: number, distancia: number): number {
    let valor = peso * 0.3 + distancia * 0.2;
    if (distancia > 100) valor *= 0.9;
    return valor;
  }
}

class ExpressoStrategy implements FreteStrategy {
  calcular(peso: number, distancia: number): number {
    return peso * 0.8 + distancia * 0.5 + 10;
  }
}

4.3. Como o contexto delega a execução para a estratégia escolhida

class CalculadoraFrete {
  private estrategias: Map<string, FreteStrategy>;

  constructor() {
    this.estrategias = new Map();
    this.estrategias.set('sedex', new SedexStrategy());
    this.estrategias.set('pac', new PacStrategy());
    this.estrategias.set('expresso', new ExpressoStrategy());
  }

  calcular(tipo: string, peso: number, distancia: number): number {
    const estrategia = this.estrategias.get(tipo);
    if (!estrategia) {
      throw new Error('Tipo de frete inválido');
    }
    return estrategia.calcular(peso, distancia);
  }
}

5. Estratégias dinâmicas: injeção e seleção em tempo de execução

5.1. Registro de estratégias em um mapa ou dicionário

O mapa permite adicionar novas estratégias sem modificar o contexto:

class GerenciadorEstrategias {
  private registro: Map<string, FreteStrategy> = new Map();

  registrar(tipo: string, estrategia: FreteStrategy): void {
    this.registro.set(tipo, estrategia);
  }

  obter(tipo: string): FreteStrategy | undefined {
    return this.registro.get(tipo);
  }
}

5.2. Seleção automática baseada em parâmetros ou regras de negócio

class SelecionadorEstrategia {
  selecionar(peso: number, distancia: number, urgencia: boolean): FreteStrategy {
    if (urgencia) return new ExpressoStrategy();
    if (distancia > 200) return new SedexStrategy();
    return new PacStrategy();
  }
}

5.3. Vantagens de desacoplar a lógica de seleção da execução

A separação permite testar a seleção independentemente da execução e vice-versa, além de facilitar a substituição de regras de negócio.

6. Testabilidade e manutenção com o padrão strategy

6.1. Testes unitários isolados para cada estratégia

// Teste para SedexStrategy
describe('SedexStrategy', () => {
  it('deve calcular frete com peso até 10kg', () => {
    const strategy = new SedexStrategy();
    expect(strategy.calcular(5, 100)).toBe(5 * 0.5 + 100 * 0.3);
  });

  it('deve adicionar taxa para peso acima de 10kg', () => {
    const strategy = new SedexStrategy();
    expect(strategy.calcular(15, 100)).toBe(15 * 0.5 + 100 * 0.3 + 15);
  });
});

6.2. Adicionando novas estratégias sem modificar código existente

Para adicionar frete marítimo, basta criar uma nova classe:

class MaritimoStrategy implements FreteStrategy {
  calcular(peso: number, distancia: number): number {
    return peso * 0.2 + distancia * 0.1 + 50;
  }
}

// Registrar no gerenciador
gerenciador.registrar('maritimo', new MaritimoStrategy());

6.3. Exemplo de evolução: novo requisito = nova classe, sem riscos

Nenhum código existente precisa ser alterado, eliminando o risco de regressão.

7. Armadilhas comuns e boas práticas

7.1. Quando o padrão strategy não é a melhor solução

  • Quando há apenas 2-3 variações simples
  • Quando as variações compartilham muito código comum (prefira Template Method)
  • Quando a seleção da estratégia é trivial e não muda

7.2. Cuidados com excesso de classes e complexidade desnecessária

Cada estratégia deve representar uma variação real de comportamento. Estratégias que diferem apenas em valores constantes podem ser substituídas por configuração.

7.3. Combinação com outros padrões (factory, dependency injection)

Use Factory Pattern para criar estratégias complexas e Dependency Injection para fornecer estratégias ao contexto, aumentando ainda mais o desacoplamento.

8. Conclusão e próximos passos

8.1. Resumo dos benefícios: código mais limpo, flexível e testável

O padrão Strategy transforma condicionais complexas em código modular, extensível e facilmente testável. Cada variação de comportamento fica isolada em sua própria classe, seguindo o princípio da responsabilidade única.

8.2. Checklist para aplicar strategy em projetos reais

  1. Identifique condicionais que crescem com novos requisitos
  2. Defina uma interface clara para o comportamento variável
  3. Extraia cada ramo para uma classe separada
  4. Implemente um mecanismo de seleção (mapa, factory, etc.)
  5. Teste cada estratégia isoladamente
  6. Documente como adicionar novas estratégias

8.3. Referências para aprofundamento (GoF, refactoring.guru)

O padrão Strategy é uma ferramenta poderosa para manter sistemas flexíveis e sustentáveis. Sua aplicação consistente transforma código frágil em arquitetura robusta, pronta para evoluir com as necessidades do negócio.

Referências