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
switchouif/elselongo 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.