Las cadenas de if/else crecen y se vuelven frágiles. El patrón Strategy encapsula cada algoritmo en su propia clase y permite intercambiar implementaciones en tiempo de ejecución sin alterar el código que las consume.
El problema: if/else infinito
Una calculadora con suma, resta, multiplicación y división suele nacer como una clase DefaultCalculator: métodos privados por operación y una función pública que elige cuál invocar con switch o cadena 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!");
}
}
}
El problema aparece cuando la calculadora necesita cubrir más operaciones binarias entre enteros: porcentaje, exponenciación, módulo, shift de bits. Cada funcionalidad nueva altera la implementación original, sube el acoplamiento y encarece el mantenimiento.
¿Qué es el patrón Strategy?
El patrón define la funcionalidad por medio de un contrato (interfaz), implementado según el contexto. La interfaz define la operación; las implementaciones concretas definen su ejecución.
El código consumidor depende de la abstracción. Cada estrategia queda aislada en su propia clase. Nuevos comportamientos entran sin alterar el código existente, alineado al Open/Closed Principle.
Definiendo el contrato
El primer paso es la interfaz del contrato de la estrategia. En la calculadora, algo que reciba dos números y devuelva el resultado:
interface BinaryOperationParameters {
firstOperand: number;
secondOperand: number;
operator: string;
}
type Result = number;
interface BinaryOperationStrategy {
calculate(
parameters: Pick<
BinaryOperationParameters,
"firstOperand" | "secondOperand"
>,
): Result;
}
Implementando estrategias concretas
Cada operación matemática se vuelve una clase que implementa BinaryOperationStrategy y ejecuta una única operación. Suma y división:
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;
}
}
El Context y la Factory
Para amarrar las estrategias, entran un Context y una Factory (o Analyzer). En ContextAnalyzer, un método evalúa el operador y retorna la Strategy correcta:
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!");
}
}
}
El contexto recibe y ejecuta la estrategia. Conoce solo el contrato, no la implementación concreta. La antigua DefaultCalculator pasa a recibir ese ContextAnalyzer por inyección:
class Calculator {
constructor(private readonly contextAnalyzer: ContextAnalyzer) {}
/**
* La implementación de calculate en Calculator no cambia por operación.
* Lo que crece es el ContextAnalyzer, que añade un case por operación nueva.
*/
public calculate(parameters: BinaryOperationParameters): Result {
const { operator, firstOperand, secondOperand } = parameters;
return this.contextAnalyzer
.getInstance(operator)
.calculate({ firstOperand, secondOperand });
}
}
¿Por qué usar Strategy?
Strategy ayuda en legado con varias reglas de negocio, cada una representada por un if y una implementación extensa. La parte común queda en el contrato; cada variación de regla queda en su propia clase; un analizador de contexto (resolver/factory) elige la estrategia.
En cada petición, el código evalúa el contexto de la operación y selecciona la implementación correspondiente al contrato.
Relación con otros patrones
- Adapter: Strategy varía comportamiento; Adapter aísla dependencias externas tras una interfaz propia.
- SOLID (OCP): Strategy es una forma de aplicar el Open/Closed Principle.