← Todos os padrões

Strategy

Behavioral · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Behavioral

O problema

Um trecho de comportamento tem várias variantes válidas, e qual delas se aplica depende de alguma condição em tempo de execução — o modo de transporte, o tipo de transação, a ordem de classificação. A primeira implementação tentadora é um único método com um grande if/else ou switch sobre essa condição. Funciona até a terceira ou quarta variante aparecer, ponto em que o método fica longo, toda mudança arrisca quebrar um branch não relacionado, e adicionar uma variante nova significa editar código que já funciona em vez de só adicionar código novo do lado.

A solução

Extrair cada variante atrás de uma interface comum, e dar ao código chamador uma forma de plugar qual implementação se aplica — trocável em tempo de execução, e cada variante é uma classe autocontida que pode ser testada, lida e alterada isoladamente.

classDiagram
    class Strategy {
        <<interface>>
    }
    class ConcreteStrategyA
    class ConcreteStrategyB
    class Context {
        -strategy
        +setStrategy(s)
        +execute()
    }
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB
    Context --> Strategy

Exemplo clássico

classic/RouteStrategy calcula uma rota entre dois pontos; DrivingRouteStrategy, WalkingRouteStrategy e PublicTransportRouteStrategy cada uma aplica um fator de desvio, velocidade e (pro transporte público) um tempo de espera fixo diferentes em cima do mesmo cálculo de distância em linha reta. Navigator é o contexto: ele guarda uma estratégia e delega a ela, e setStrategy(...) deixa quem chama trocar o modo de viagem pra mesma jornada sem tocar no próprio Navigator. NavigatorTest verifica tanto a matemática de cada estratégia quanto que trocar de estratégia de fato muda o resultado pro mesmo par origem/destino.

Exemplo aplicado: cálculo de tarifa por tipo de transação

applied/FeeCalculator busca uma FeeCalculationStrategy por TransactionType em vez de ramificar sobre ele: PixFeeStrategy é gratuita (o BACEN exige PIX gratuito entre pessoas físicas), TedFeeStrategy cobra uma tarifa fixa independente do valor, e BoletoFeeStrategy cobra um percentual com um piso mínimo. Esse é precisamente o cenário pro qual o padrão existe: um gateway de pagamento real adicionando um quarto tipo de transação depois significa adicionar uma classe de estratégia nova, não reabrir um método de cálculo de tarifa do qual todo tipo de transação já existente depende. FeeCalculatorTest cobre as três estratégias mais o caso de falha de "tipo não registrado".

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :behavioral:strategy:jacocoTestReport

Relatório em behavioral/strategy/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/designpatterns/behavioral/strategy/classic/NavigatorTest.java
package com.designpatterns.behavioral.strategy.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class NavigatorTest {

    private final Location origin = new Location(0, 0);
    private final Location destination = new Location(3, 4); // straight-line distance = 5km

    @Test
    void drivingRouteAppliesTheRoadDetourFactor() {
        Navigator navigator = new Navigator(new DrivingRouteStrategy());

        Route route = navigator.route(origin, destination);

        assertThat(route.distanceKm()).isEqualTo(5 * 1.3);
        assertThat(route.description()).isEqualTo("driving");
    }

    @Test
    void swappingTheStrategyChangesTheRouteForTheSameTrip() {
        Navigator navigator = new Navigator(new WalkingRouteStrategy());
        Route walking = navigator.route(origin, destination);

        navigator.setStrategy(new PublicTransportRouteStrategy());
        Route transit = navigator.route(origin, destination);

        assertThat(walking.distanceKm()).isNotEqualTo(transit.distanceKm());
        assertThat(walking.estimatedMinutes()).isNotEqualTo(transit.estimatedMinutes());
    }
}
src/test/java/com/designpatterns/behavioral/strategy/applied/FeeCalculatorTest.java
package com.designpatterns.behavioral.strategy.applied;

import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class FeeCalculatorTest {

    private final FeeCalculator calculator = FeeCalculator.withDefaultStrategies();

    @Test
    void pixTransfersAreFree() {
        assertThat(calculator.calculateFeeCents(TransactionType.PIX, 500_00L)).isZero();
    }

    @Test
    void tedChargesAFlatFeeRegardlessOfAmount() {
        assertThat(calculator.calculateFeeCents(TransactionType.TED, 100_00L)).isEqualTo(1000L);
        assertThat(calculator.calculateFeeCents(TransactionType.TED, 50_000_00L)).isEqualTo(1000L);
    }

    @Test
    void boletoChargesThePercentageFeeAboveTheMinimum() {
        assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 100_000_00L)).isEqualTo(2_000_00L);
    }

    @Test
    void boletoFallsBackToTheMinimumFeeForSmallAmounts() {
        assertThat(calculator.calculateFeeCents(TransactionType.BOLETO, 1_00L)).isEqualTo(350L);
    }

    @Test
    void rejectsAnUnregisteredTransactionType() {
        FeeCalculator calculatorWithoutBoleto = new FeeCalculator(Map.of(TransactionType.PIX, new PixFeeStrategy()));

        assertThatThrownBy(() -> calculatorWithoutBoleto.calculateFeeCents(TransactionType.BOLETO, 100L))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →