Design Patterns

Strategy

Encapsula algoritmos intercambiáveis e os torna substituíveis em tempo de execução

Conceito e intenção

Strategy define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis. O padrão permite que o algoritmo varie independentemente dos clientes que o utilizam.

O problema motivador é o crescimento descontrolado de condicionais. Imagine um sistema de pagamento que começa com cartão de crédito e, com o tempo, precisa suportar boleto, Pix, PayPal e criptomoeda. Sem Strategy, o método processar() vira um if/else com dezenas de ramos. Cada novo canal exige modificar e retestar código existente — violando o Princípio Aberto/Fechado.

Strategy resolve isso extraindo cada variação para sua própria classe, todas implementando a mesma interface. O contexto recebe a estratégia por injeção e não precisa ser alterado quando um novo canal surge.


Estrutura

┌─────────────────────┐       ┌──────────────────┐
│       Context       │──────▶│ <<interface>>    │
│  - strategy         │       │    Strategy      │
│  + execute()        │       │  + execute()     │
└─────────────────────┘       └──────────────────┘

                          ┌────────────┼────────────┐
                   ┌──────┴──┐   ┌────┴────┐  ┌────┴────┐
                   │Concrete │   │Concrete │  │Concrete │
                   │  A      │   │   B     │  │   C     │
                   └─────────┘   └─────────┘  └─────────┘

Participantes:

  • Strategy — interface comum a todos os algoritmos
  • ConcreteStrategy — implementação específica do algoritmo
  • Context — mantém referência à Strategy ativa; delega o trabalho a ela

Implementação Java — processadores de desconto

// Strategy — interface comum para todos os tipos de desconto
public interface DiscountStrategy {
    Money apply(Money subtotal, Customer customer);
}

// Sem desconto — strategy nula explícita (Null Object)
public class NoDiscount implements DiscountStrategy {
    @Override
    public Money apply(Money subtotal, Customer customer) {
        return subtotal;
    }
}

// Desconto percentual simples
public class PercentageDiscount implements DiscountStrategy {
    private final double percentage;

    public PercentageDiscount(double percentage) {
        this.percentage = percentage;
    }

    @Override
    public Money apply(Money subtotal, Customer customer) {
        return subtotal.multiply(1 - percentage / 100);
    }
}

// Desconto para clientes VIP com fidelidade
public class LoyaltyDiscount implements DiscountStrategy {
    private static final int POINTS_PER_REAL = 10;

    @Override
    public Money apply(Money subtotal, Customer customer) {
        int points = customer.getLoyaltyPoints();
        // cada 1000 pontos = R$5 de desconto
        Money discount = Money.of(points / POINTS_PER_REAL / 100.0);
        return subtotal.subtract(discount.min(subtotal.multiply(0.15))); // limite 15%
    }
}

// Desconto por cupom (validação externa)
public class CouponDiscount implements DiscountStrategy {
    private final CouponService couponService;
    private final String couponCode;

    public CouponDiscount(CouponService couponService, String couponCode) {
        this.couponService = couponService;
        this.couponCode    = couponCode;
    }

    @Override
    public Money apply(Money subtotal, Customer customer) {
        return couponService.calculate(couponCode, subtotal);
    }
}

// Context — usa a estratégia sem conhecer sua implementação
public class OrderPricer {
    private final DiscountStrategy discountStrategy;
    private final TaxCalculator taxCalculator;

    public OrderPricer(DiscountStrategy discountStrategy, TaxCalculator taxCalculator) {
        this.discountStrategy = discountStrategy;
        this.taxCalculator    = taxCalculator;
    }

    public OrderSummary price(Order order, Customer customer) {
        Money subtotal      = order.subtotal();
        Money afterDiscount = discountStrategy.apply(subtotal, customer);
        Money tax           = taxCalculator.calculate(afterDiscount);
        Money total         = afterDiscount.add(tax);
        return new OrderSummary(subtotal, afterDiscount, tax, total);
    }
}

// Uso — escolha da estratégia em tempo de execução
DiscountStrategy strategy = switch (customer.getType()) {
    case VIP     -> new LoyaltyDiscount();
    case COUPON  -> new CouponDiscount(couponService, request.getCouponCode());
    default      -> new NoDiscount();
};

OrderPricer pricer = new OrderPricer(strategy, new BrazilTax());
OrderSummary summary = pricer.price(order, customer);

Implementação TypeScript — mesmo exemplo

// Strategy — interface comum
interface DiscountStrategy {
  apply(subtotal: number, customer: Customer): number;
}

// Null Object
class NoDiscount implements DiscountStrategy {
  apply(subtotal: number): number {
    return subtotal;
  }
}

// Desconto percentual
class PercentageDiscount implements DiscountStrategy {
  constructor(private readonly percentage: number) {}

  apply(subtotal: number): number {
    return subtotal * (1 - this.percentage / 100);
  }
}

// Desconto por fidelidade
class LoyaltyDiscount implements DiscountStrategy {
  apply(subtotal: number, customer: Customer): number {
    const maxDiscount = subtotal * 0.15;
    const discount = (customer.loyaltyPoints / 1000) * 5;
    return subtotal - Math.min(discount, maxDiscount);
  }
}

// Context
class OrderPricer {
  constructor(
    private readonly discountStrategy: DiscountStrategy,
    private readonly taxRate: number
  ) {}

  price(subtotal: number, customer: Customer): OrderSummary {
    const afterDiscount = this.discountStrategy.apply(subtotal, customer);
    const tax           = afterDiscount * this.taxRate;
    const total         = afterDiscount + tax;
    return { subtotal, afterDiscount, tax, total };
  }
}

// Versão funcional — funções como strategies (sem classes)
type DiscountFn = (subtotal: number, customer: Customer) => number;

const noDiscount: DiscountFn       = (s) => s;
const tenPercent: DiscountFn       = (s) => s * 0.9;
const loyaltyDiscount: DiscountFn  = (s, c) => s - Math.min((c.loyaltyPoints / 1000) * 5, s * 0.15);

// Composição funcional de strategies
const applyDiscounts = (...fns: DiscountFn[]) =>
  (subtotal: number, customer: Customer) =>
    fns.reduce((acc, fn) => fn(acc, customer), subtotal);

const combined = applyDiscounts(tenPercent, loyaltyDiscount);

Variações e extensões

Strategy com lambda (Java): Em Java, qualquer interface funcional pode ser implementada como lambda. DiscountStrategy tem um único método, então funciona diretamente:

// Sem criar classe concreta
DiscountStrategy blackFriday = (subtotal, customer) -> subtotal.multiply(0.7);
DiscountStrategy freeShipping = (subtotal, customer) ->
    subtotal.isGreaterThan(Money.of(200)) ? subtotal : subtotal.add(Money.of(15));

Registry de strategies: Útil quando a seleção é feita por string (ex: configuração externa):

public class DiscountRegistry {
    private final Map<String, DiscountStrategy> strategies = new HashMap<>();

    public void register(String key, DiscountStrategy strategy) {
        strategies.put(key, strategy);
    }

    public DiscountStrategy get(String key) {
        return strategies.getOrDefault(key, new NoDiscount());
    }
}

Strategy com estado: Se a estratégia precisar de contexto persistente entre chamadas (ex: acumulador de desconto progressivo), ela pode manter estado interno — desde que o Context não acesse esse estado diretamente.


No mundo real

Java Comparator é a implementação mais usada do padrão Strategy no SDK. Collections.sort(list, comparator) aceita qualquer implementação de Comparator — a estratégia de ordenação é completamente intercambiável:

// Três strategies diferentes de ordenação
orders.sort(Comparator.comparing(Order::getCreatedAt));
orders.sort(Comparator.comparing(Order::getTotal).reversed());
orders.sort(Comparator.comparing(Order::getCustomerId).thenComparing(Order::getCreatedAt));

Spring Security usa AuthenticationStrategy e AccessDecisionStrategy para decidir como autenticar e autorizar requisições. Troca de JWT para OAuth2 = trocar a estratégia.

Hibernate NamingStrategy permite personalizar como nomes de entidades são mapeados para tabelas — a estratégia padrão usa snake_case, mas você pode injetar outra.

React: Props como funções (renderItem, keyExtractor, onSort) são o equivalente funcional de Strategy em componentes. O componente pai define o comportamento; o componente filho executa.

Jackson PropertyNamingStrategy controla como campos Java são serializados para JSON (SNAKE_CASE, UPPER_CAMEL_CASE, ou implementação customizada).


Quando usar

  • O mesmo objeto precisa ter comportamentos diferentes dependendo do contexto
  • Há um switch ou if/else longo baseado em tipo/configuração dentro de um método
  • Você precisa trocar algoritmos em tempo de execução (ex: baseado em configuração do usuário)
  • Você quer testar variações de um algoritmo isoladamente
  • Precisa adicionar novos comportamentos sem modificar código existente (Open/Closed Principle)

Quando NÃO usar

  • Quando há apenas dois ou três variações simples que nunca vão crescer — um if/else é mais legível
  • Quando as strategies precisam compartilhar muita lógica — considere Template Method (herança em vez de composição)
  • Quando a criação de dezenas de classes para variações triviais é over-engineering — funções puras ou lambdas bastam
  • Quando o contexto precisa acessar internals da strategy — isso quebra o encapsulamento e indica design incorreto

Combinações com outros padrões

Strategy + Factory Method: Use uma Factory para selecionar a Strategy correta com base em parâmetros:

public class DiscountStrategyFactory {
    public static DiscountStrategy create(Cart cart, Customer customer) {
        if (cart.hasCoupon())             return new CouponDiscount(cart.getCoupon());
        if (customer.getType() == VIP)    return new LoyaltyDiscount();
        if (cart.total().gt(Money.of(500))) return new PercentageDiscount(10);
        return new NoDiscount();
    }
}

Strategy + Decorator: Decorators adicionam comportamento transversal (logging, métricas) ao redor de uma Strategy sem modificá-la:

public class MeteredDiscount implements DiscountStrategy {
    private final DiscountStrategy delegate;
    private final MeterRegistry registry;

    public MeteredDiscount(DiscountStrategy delegate, MeterRegistry registry) {
        this.delegate = delegate;
        this.registry = registry;
    }

    @Override
    public Money apply(Money subtotal, Customer customer) {
        registry.counter("discount.applied", "type", delegate.getClass().getSimpleName()).increment();
        return delegate.apply(subtotal, customer);
    }
}

Strategy + Template Method: Template Method define o esqueleto do algoritmo; Strategy troca etapas inteiras. Quando o algoritmo tem múltiplos passos fixos e apenas um varia, use Template Method. Quando o algoritmo inteiro pode ser trocado, use Strategy.