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.