Design Patterns: Strategy

Cadeias de if/else crescem e ficam frágeis. O Strategy isola cada algoritmo atrás de um contrato.

Cadeias de if/else crescem e ficam frágeis. O padrão Strategy encapsula cada algoritmo em sua própria classe e permite trocar implementações em tempo de execução sem alterar o código que as consome.

O problema: if/else infinito

Uma calculadora com soma, subtração, multiplicação e divisão costuma nascer como uma classe DefaultCalculator: métodos privados por operação e uma função pública que escolhe qual invocar com switch ou cadeia de if/else.

class DefaultCalculator {
  public calculate(parameters: BinaryOperationParameters): Result {
    const { operator, firstOperand, secondOperand } = parameters;

    switch (operator) {
      case "*":
        return firstOperand * secondOperand;
      case "+":
        return firstOperand + secondOperand;
      case "-":
        return firstOperand - secondOperand;
      case "/":
        return firstOperand / secondOperand;
      case "**":
        return firstOperand ** secondOperand;
      case "%":
        return firstOperand % secondOperand;
      default:
        throw new Error("Operator not found!");
    }
  }
}

O problema aparece quando a calculadora precisa cobrir mais operações binárias entre inteiros: percentual, exponenciação, módulo, shift de bits. Cada funcionalidade nova altera a implementação original, sobe o acoplamento e encarece a manutenção.

O que é o padrão Strategy?

O padrão define a funcionalidade por meio de um contrato (interface), implementado conforme o contexto. A interface define a operação; as implementações concretas definem sua execução.

O código consumidor depende da abstração. Cada estratégia fica isolada em sua própria classe. Novos comportamentos entram sem alterar o código existente, alinhado ao Open/Closed Principle.

Definindo o contrato

O primeiro passo é a interface do contrato da estratégia. Na calculadora, algo que receba dois números e retorne o resultado:

interface BinaryOperationParameters {
  firstOperand: number;
  secondOperand: number;
  operator: string;
}

type Result = number;

interface BinaryOperationStrategy {
  calculate(
    parameters: Pick<
      BinaryOperationParameters,
      "firstOperand" | "secondOperand"
    >,
  ): Result;
}

Implementando estratégias concretas

Cada operação matemática vira uma classe que implementa BinaryOperationStrategy e executa uma única operação. Soma e divisão:

class Sum implements BinaryOperationStrategy {
  public calculate(
    parameters: Pick<
      BinaryOperationParameters,
      "firstOperand" | "secondOperand"
    >,
  ): Result {
    const { firstOperand, secondOperand } = parameters;
    return firstOperand + secondOperand;
  }
}

class Division implements BinaryOperationStrategy {
  public calculate(
    parameters: Pick<
      BinaryOperationParameters,
      "firstOperand" | "secondOperand"
    >,
  ): Result {
    const { firstOperand, secondOperand } = parameters;
    if (secondOperand === 0) {
      throw new Error("Division by zero is not allowed!");
    }
    return firstOperand / secondOperand;
  }
}

O Context e a Factory

Para amarrar as estratégias, entram um Context e uma Factory (ou Analyzer). Em ContextAnalyzer, um método avalia o operador e retorna a Strategy correta:

class ContextAnalyzer {
  public getInstance(operator: string): BinaryOperationStrategy {
    switch (operator) {
      case "*":
        return new Multiplication();
      case "+":
        return new Sum();
      case "-":
        return new Subtraction();
      case "/":
        return new Division();
      case "%":
        return new Percent();
      case "**":
        return new Pow();
      default:
        throw new Error("Operator not found!");
    }
  }
}

O contexto recebe e executa a estratégia. Ele conhece apenas o contrato, não a implementação concreta. A antiga DefaultCalculator passa a receber esse ContextAnalyzer por injeção:

class Calculator {
  constructor(private readonly contextAnalyzer: ContextAnalyzer) {}

  /**
   * A implementação de calculate em Calculator não muda a cada operação.
   * O que cresce é o ContextAnalyzer, que adiciona um case por operação nova.
   */
  public calculate(parameters: BinaryOperationParameters): Result {
    const { operator, firstOperand, secondOperand } = parameters;
    return this.contextAnalyzer
      .getInstance(operator)
      .calculate({ firstOperand, secondOperand });
  }
}

Por que usar o Strategy?

O Strategy ajuda em legado com várias regras de negócio, cada uma representada por um if e uma implementação extensa. A parte comum fica no contrato; cada variação de regra fica em sua própria classe; um analisador de contexto (resolver/factory) escolhe a estratégia.

A cada requisição, o código avalia o contexto da operação e seleciona a implementação correspondente ao contrato.

Relação com outros padrões

  • Adapter: o Strategy varia comportamento; o Adapter isola dependências externas atrás de uma interface própria.
  • SOLID (OCP): o Strategy é uma forma de aplicar o Open/Closed Principle.