if/else chains grow and turn fragile. The Strategy pattern encapsulates each algorithm in its own class and lets implementations be swapped at runtime without changing the consuming code.
The problem: endless if/else
A calculator with addition, subtraction, multiplication, and division usually starts as a DefaultCalculator class: private methods per operation and a public function choosing which to invoke with a switch or if/else chain.
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!");
}
}
}
The problem appears when the calculator must cover more binary operations between integers: percent, exponentiation, modulo, bit shifts. Each new feature changes the original implementation, raises coupling, and makes maintenance more expensive.
What is the Strategy pattern?
The pattern defines functionality through a contract (interface), implemented according to context. The interface defines the operation; concrete implementations define its execution.
Consuming code depends on the abstraction. Each strategy stays isolated in its own class. New behaviors arrive without changing existing code, aligned with the Open/Closed Principle.
Defining the contract
The first step is the strategy contract interface. For the calculator, something receiving two numbers and returning the result:
interface BinaryOperationParameters {
firstOperand: number;
secondOperand: number;
operator: string;
}
type Result = number;
interface BinaryOperationStrategy {
calculate(
parameters: Pick<
BinaryOperationParameters,
"firstOperand" | "secondOperand"
>,
): Result;
}
Implementing concrete strategies
Each math operation becomes a class implementing BinaryOperationStrategy and executing a single operation. Addition and division:
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;
}
}
The Context and the Factory
To tie strategies together, a Context and a Factory (or Analyzer) come in. In ContextAnalyzer, a method evaluates the operator and returns the right Strategy:
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!");
}
}
}
The context receives and executes the strategy. It knows only the contract, not the concrete implementation. The old DefaultCalculator starts receiving this ContextAnalyzer by injection:
class Calculator {
constructor(private readonly contextAnalyzer: ContextAnalyzer) {}
/**
* The calculate implementation in Calculator does not change per operation.
* What grows is the ContextAnalyzer, which adds one case per new operation.
*/
public calculate(parameters: BinaryOperationParameters): Result {
const { operator, firstOperand, secondOperand } = parameters;
return this.contextAnalyzer
.getInstance(operator)
.calculate({ firstOperand, secondOperand });
}
}
Why use Strategy?
Strategy helps in legacy code with several business rules, each represented by an if and a long implementation. The shared part stays in the contract; each rule variation stays in its own class; a context analyzer (resolver/factory) picks the strategy.
On each request, the code evaluates the operation context and selects the implementation matching the contract.
Relation to other patterns
- Adapter: Strategy varies behavior; Adapter isolates external dependencies behind an owned interface.
- SOLID (OCP): Strategy is one way to apply the Open/Closed Principle.