Pular para o conteúdo

Strategy

Definir uma família de algoritmos, encapsular cada um deles e torná-los intercambiáveis. O Strategy permite que o algoritmo varie independentemente dos clientes que o utilizam.

Uma classe possui um comportamento que varia conforme condições, e essa variação é codificada como um bloco de condicionais (if/else, switch). A cada nova variante, edita-se a classe — violando o OCP, inchando o método, misturando responsabilidades (SRP - Single Responsibility Principle) e dificultando teste isolado de cada ramo.

  • Strategy: interface que declara a operação comum a todos os algoritmos.
  • ConcreteStrategy: cada implementação encapsula um algoritmo específico.
  • Context: mantém uma referência a uma Strategy e delega a ela, sem conhecer a implementação concreta — recebe a estratégia por composição (idealmente por injeção, apoiando o DIP - Dependency Inversion Principle).

O ponto essencial: o Context depende da abstração, não do algoritmo concreto. Trocar a estratégia é trocar o objeto injetado.

  • Materialização do OCP: adicionar um novo algoritmo é criar uma nova classe/lambda que implementa a interface — sem tocar no Context nem nas estratégias existentes. O sistema fica aberto para extensão e fechado para modificação.
  • Destino do refactoring “Replace Conditional with Polymorphism” (ver Técnicas de Refactoring): um switch (tipo) espalhado vira um conjunto de estratégias polimórficas selecionadas por um mapa ou factory. É a cura de vários Code Smells (Switch Statements, Long Method), tema tratado em Code Smells e SOLID.
  • Apoia o DIP - Dependency Inversion Principle: o Context depende de uma abstração (Strategy), e as políticas concretas são fornecidas de fora, invertendo a dependência.

Prós

  • Elimina condicionais de seleção de algoritmo.
  • Cada algoritmo é testável isoladamente.
  • Troca em runtime; composição em vez de herança.

Contras

  • Mais classes/objetos e uma indireção a mais.
  • O cliente precisa conhecer as estratégias disponíveis para escolher (mitigável com factory/registry).
  • Overhead injustificado se as variações são poucas, estáveis e triviais.

Quando há uma única implementação sem variação prevista, ou quando a diferença entre “algoritmos” é tão pequena que um parâmetro simples resolve. Não crie uma hierarquia de estratégias para dois ifs que nunca vão crescer.

public class CheckoutService {
public BigDecimal computeDiscount(String customerType, BigDecimal amount) {
if (customerType.equals("REGULAR")) {
return BigDecimal.ZERO;
} else if (customerType.equals("PREMIUM")) {
return amount.multiply(new BigDecimal("0.10"));
} else if (customerType.equals("VIP")) {
return amount.multiply(new BigDecimal("0.20"));
}
throw new IllegalArgumentException("tipo desconhecido");
}
}

Cada novo tipo de cliente exige editar este método.

@FunctionalInterface
public interface DiscountStrategy {
BigDecimal discountFor(BigDecimal amount);
}
public class RegularDiscount implements DiscountStrategy {
public BigDecimal discountFor(BigDecimal amount) {
return BigDecimal.ZERO;
}
}
public class PremiumDiscount implements DiscountStrategy {
public BigDecimal discountFor(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.10"));
}
}
public class VipDiscount implements DiscountStrategy {
public BigDecimal discountFor(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.20"));
}
}

Context por composição/injeção:

public class CheckoutService {
private final DiscountStrategy strategy;
public CheckoutService(DiscountStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal finalPrice(BigDecimal amount) {
return amount.subtract(strategy.discountFor(amount));
}
}

Seleção desacoplada via registry:

public class DiscountResolver {
private final Map<String, DiscountStrategy> strategies = Map.of(
"REGULAR", new RegularDiscount(),
"PREMIUM", new PremiumDiscount(),
"VIP", new VipDiscount()
);
public DiscountStrategy resolve(String customerType) {
DiscountStrategy s = strategies.get(customerType);
if (s == null) {
throw new IllegalArgumentException("tipo desconhecido: " + customerType);
}
return s;
}
}

Como DiscountStrategy é uma interface funcional, estratégias simples dispensam classes:

DiscountStrategy blackFriday = amount -> amount.multiply(new BigDecimal("0.35"));
CheckoutService service = new CheckoutService(blackFriday);

Em Java moderno, muitas Strategies triviais viram lambdas ou method references, reduzindo a cerimônia do padrão à sua essência: um algoritmo passado como valor.

classDiagram
    class DiscountStrategy {
        <<interface>>
        +discountFor(amount) BigDecimal
    }
    class RegularDiscount {
        +discountFor(amount) BigDecimal
    }
    class PremiumDiscount {
        +discountFor(amount) BigDecimal
    }
    class VipDiscount {
        +discountFor(amount) BigDecimal
    }
    class CheckoutService {
        -DiscountStrategy strategy
        +finalPrice(amount) BigDecimal
    }
    DiscountStrategy <|.. RegularDiscount
    DiscountStrategy <|.. PremiumDiscount
    DiscountStrategy <|.. VipDiscount
    CheckoutService o--> DiscountStrategy
  • Vazar a seleção da estratégia para dentro do Context com um switch que escolhe a strategy — isso só empurra a condicional para outro lugar. Centralize a escolha num factory/registry ou injete diretamente.
  • Estratégias com interfaces divergentes, forçando instanceof no cliente — quebra a intercambialidade.
  • Confundir com State: mesma UML, intenção diferente. Strategy é escolhida de fora e não conhece as outras; State controla suas transições.
  • Aplicar o padrão para uma variação única e estável — indireção sem retorno.

1. Por que se diz que Strategy é a materialização do OCP?

Resposta

Porque adicionar um novo algoritmo é criar uma nova implementação da interface Strategy, sem modificar o Context nem as estratégias existentes. O sistema fica aberto para extensão (novas estratégias) e fechado para modificação (código estável), que é exatamente o enunciado do OCP.

2. Qual refactoring conduz naturalmente ao Strategy e qual smell ele cura?

Resposta

Replace Conditional with Polymorphism: um switch/if-else que seleciona comportamento por tipo vira estratégias polimórficas. Ataca os smells Switch Statements e Long Method.

3. Como o Strategy apoia o DIP?

Resposta

O Context depende da abstração Strategy, não de algoritmos concretos, e recebe a estratégia por injeção. A dependência aponta para a interface (política), invertendo o acoplamento em relação às implementações de baixo nível.

4. Quando uma Strategy pode ser apenas uma lambda em Java?

Resposta

Quando a interface da estratégia é funcional (um único método abstrato). Estratégias simples e sem estado dispensam classes concretas e podem ser expressas como lambdas ou method references, reduzindo boilerplate sem perder a intenção do padrão.