Have you ever found yourself trapped in a labyrinth of if-else statements? You start with a simple condition, but as new requirements pour in, the logic branches out, growing into a tangled, unmanageable beast. This is a common scenario, especially when dealing with tasks that have multiple variations or algorithms.
Let’s consider a payroll system. You need to calculate salaries for different types of employees: full-time, hourly, task-based, freelancers, and interns. A naive approach might look like this:
public class SalaryCalculator {
public BigDecimal calculate(Employee employee) {
if (employee.getType() == EmployeeType.FULL_TIME) {
// Full-time logic: base salary + bonus
return employee.getBaseSalary().add(employee.getBonus());
} else if (employee.getType() == EmployeeType.HOURLY) {
// Hourly logic: hours worked * hourly rate
return employee.getHourlyRate().multiply(BigDecimal.valueOf(employee.getHoursWorked()));
} else if (employee.getType() == EmployeeType.TASK_BASED) {
// Task-based logic: tasks completed * rate per task
return employee.getPayPerTask().multiply(BigDecimal.valueOf(employee.getTasksCompleted()));
} else if (employee.getType() == EmployeeType.FREELANCER) {
// Freelancer logic: gross amount - tax - commission
BigDecimal tax = employee.getGrossAmount().multiply(new BigDecimal("0.10"));
BigDecimal commission = employee.getGrossAmount().multiply(new BigDecimal("0.05"));
return employee.getGrossAmount().subtract(tax).subtract(commission);
} else if (employee.getType() == EmployeeType.INTERN) {
// Interns get a fixed stipend
return new BigDecimal("1000.00");
}
// And what if a new employee type is added tomorrow? Another 'else if'...
throw new IllegalArgumentException("Unknown employee type");
}
}
This code works, but it’s a maintenance nightmare.
- It violates the Single Responsibility Principle (SRP): The
SalaryCalculatorclass is responsible for every single salary calculation algorithm. - It violates the Open/Closed Principle (OCP): To add a new employee type (e.g., “Commissioned”), you must modify this giant
if-elseblock, risking the introduction of bugs into existing, working logic. - It’s hard to test: You need to create complex test setups to cover every single branch of this monolithic method.
This is where the Strategy Design Pattern comes to the rescue. It provides an elegant way to clean up this mess.
What is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one in a separate class, and make their objects interchangeable. Instead of implementing a single algorithm directly, the code receives runtime instructions as to which in a family of algorithms to use.
Think of a golfer. A golfer carries a bag with different clubs (drivers, irons, putters). Depending on the terrain, the distance to the hole, and the desired shot, the golfer selects a strategy by choosing the appropriate club. The action (swinging) is the same, but the chosen club (the strategy) determines the outcome.
Here’s a high-level overview of the pattern’s components:
mindmap
root((Strategy Pattern))
Context
(PayrollService)
Maintains a reference to a Strategy object.
Delegates the work to the strategy.
Strategy Interface
(ISalaryCalculationStrategy)
Declares a common interface for all supported algorithms.
Concrete Strategies
(FullTimeStrategy)
(HourlyStrategy)
(FreelancerStrategy)
...
Each class implements a specific algorithm.
Refactoring the Salary Calculator with the Strategy Pattern
Let’s apply this pattern to our payroll system. We’ll refactor the monolithic SalaryCalculator into a flexible, maintainable, and scalable solution.
Step 1: Define the Project Structure
First, let’s organize our files. We’ll create a dedicated package for our strategies.
src/
└── com/
└── payroll/
├── Employee.java
├── EmployeeType.java
├── PayrollService.java
└── strategy/
├── ISalaryCalculationStrategy.java
├── FullTimeSalaryStrategy.java
├── HourlySalaryStrategy.java
├── TaskBasedSalaryStrategy.java
├── FreelancerSalaryStrategy.java
└── InternSalaryStrategy.java
Step 2: Create the Strategy Interface
This interface defines the single method that all our calculation algorithms will implement.
[!NOTE] An interface in this context is a contract. It guarantees that every “strategy” class we create will have a
calculatemethod, allowing ourPayrollServiceto use them interchangeably without knowing the specific details of any single one.
// src/com/payroll/strategy/ISalaryCalculationStrategy.java
package com.payroll.strategy;
import com.payroll.Employee;
import java.math.BigDecimal;
/**
* The Strategy interface declares the method for calculating salary,
* which is common to all concrete strategy classes.
*/
public interface ISalaryCalculationStrategy {
BigDecimal calculate(Employee employee);
}
Step 3: Implement Concrete Strategies
Now, we extract each branch of our original if-else statement into its own class. Each class implements the ISalaryCalculationStrategy interface.
Full-Time Strategy:
// src/com/payroll/strategy/FullTimeSalaryStrategy.java
package com.payroll.strategy;
import com.payroll.Employee;
import java.math.BigDecimal;
public class FullTimeSalaryStrategy implements ISalaryCalculationStrategy {
@Override
public BigDecimal calculate(Employee employee) {
System.out.println("Calculating salary for a full-time employee...");
return employee.getBaseSalary().add(employee.getBonus());
}
}
Hourly Strategy:
// src/com/payroll/strategy/HourlySalaryStrategy.java
package com.payroll.strategy;
import com.payroll.Employee;
import java.math.BigDecimal;
public class HourlySalaryStrategy implements ISalaryCalculationStrategy {
@Override
public BigDecimal calculate(Employee employee) {
System.out.println("Calculating salary for an hourly employee...");
return employee.getHourlyRate().multiply(BigDecimal.valueOf(employee.getHoursWorked()));
}
}
Freelancer Strategy:
// src/com/payroll/strategy/FreelancerSalaryStrategy.java
package com.payroll.strategy;
import com.payroll.Employee;
import java.math.BigDecimal;
public class FreelancerSalaryStrategy implements ISalaryCalculationStrategy {
private static final BigDecimal TAX_RATE = new BigDecimal("0.10");
private static final BigDecimal COMMISSION_RATE = new BigDecimal("0.05");
@Override
public BigDecimal calculate(Employee employee) {
System.out.println("Calculating salary for a freelancer...");
BigDecimal gross = employee.getGrossAmount();
BigDecimal tax = gross.multiply(TAX_RATE);
BigDecimal commission = gross.multiply(COMMISSION_RATE);
return gross.subtract(tax).subtract(commission);
}
}
…and so on for TaskBasedSalaryStrategy and InternSalaryStrategy. Each class now has a single, clear responsibility.
Step 4: Create the Context
The Context is the class that will use our strategies. In our case, this is the PayrollService. It doesn’t perform the calculation itself; it delegates the task to the strategy object it holds.
// src/com/payroll/PayrollService.java
package com.payroll;
import com.payroll.strategy.ISalaryCalculationStrategy;
import java.math.BigDecimal;
/**
* The Context class. It is configured with a Concrete Strategy object
* and maintains a reference to it. It doesn't know the concrete type of a
* strategy. It should work with all strategies via the Strategy interface.
*/
public class PayrollService {
private ISalaryCalculationStrategy strategy;
// The strategy can be set at runtime
public void setStrategy(ISalaryCalculationStrategy strategy) {
this.strategy = strategy;
}
public BigDecimal calculateSalary(Employee employee) {
if (strategy == null) {
throw new IllegalStateException("Strategy not set!");
}
return strategy.calculate(employee);
}
}
Notice how clean this is. The PayrollService has no idea how the salary is calculated. It only knows that it has a strategy object that can do the job.
Step 5: Selecting the Right Strategy
But how does the PayrollService get the correct strategy? We need a mechanism to select the strategy based on the EmployeeType. A Simple Factory is a great pattern for this job.
// src/com/payroll/strategy/SalaryStrategyFactory.java
package com.payroll.strategy;
import com.payroll.EmployeeType;
public class SalaryStrategyFactory {
public static ISalaryCalculationStrategy getStrategy(EmployeeType type) {
switch (type) {
case FULL_TIME:
return new FullTimeSalaryStrategy();
case HOURLY:
return new HourlySalaryStrategy();
case TASK_BASED:
return new TaskBasedSalaryStrategy();
case FREELANCER:
return new FreelancerSalaryStrategy();
case INTERN:
return new InternSalaryStrategy();
default:
throw new IllegalArgumentException("Unsupported employee type: " + type);
}
}
}
This factory encapsulates the selection logic. If we add a new employee type, we only need to update the EmployeeType enum, create a new strategy class, and add one line to this factory. The PayrollService remains untouched!
Step 6: Putting It All Together (The Client Code)
Now let’s see how the client code changes.
- // Before: The client is coupled to the complex calculation logic
- SalaryCalculator calculator = new SalaryCalculator();
- BigDecimal salary = calculator.calculate(employee);
+ // After: The client delegates the work cleanly
+ // 1. Create the context
+ PayrollService payrollService = new PayrollService();
+
+ // 2. Use the factory to get the correct strategy based on employee type
+ ISalaryCalculationStrategy strategy = SalaryStrategyFactory.getStrategy(employee.getType());
+
+ // 3. Set the strategy in the context
+ payrollService.setStrategy(strategy);
+
+ // 4. Execute the calculation
+ BigDecimal salary = payrollService.calculateSalary(employee);
The client code is now declarative and easy to read. It orchestrates the process without getting bogged down in implementation details.
Visualizing the Final Design
Here are two diagrams that illustrate our new, flexible system.
Class Diagram: This shows the static structure and relationships between our classes.
classDiagram
class PayrollService {
-ISalaryCalculationStrategy strategy
+setStrategy(ISalaryCalculationStrategy)
+calculateSalary(Employee) BigDecimal
}
class SalaryStrategyFactory {
<<static>>
+getStrategy(EmployeeType) ISalaryCalculationStrategy
}
class Employee {
-EmployeeType type
}
<<interface>> ISalaryCalculationStrategy {
+calculate(Employee) BigDecimal
}
class FullTimeSalaryStrategy {
+calculate(Employee) BigDecimal
}
class HourlySalaryStrategy {
+calculate(Employee) BigDecimal
}
class FreelancerSalaryStrategy {
+calculate(Employee) BigDecimal
}
PayrollService o-- ISalaryCalculationStrategy : uses
SalaryStrategyFactory ..> ISalaryCalculationStrategy : creates
FullTimeSalaryStrategy --|> ISalaryCalculationStrategy : implements
HourlySalaryStrategy --|> ISalaryCalculationStrategy : implements
FreelancerSalaryStrategy --|> ISalaryCalculationStrategy : implements
PayrollService ..> Employee
SalaryStrategyFactory ..> EmployeeType
Sequence Diagram: This shows how the objects interact at runtime to perform a calculation.
sequenceDiagram
participant Client
participant PayrollService
participant SalaryStrategyFactory
participant ConcreteStrategy
Client->>SalaryStrategyFactory: getStrategy(employee.getType())
SalaryStrategyFactory-->>Client: returns strategy instance
Client->>PayrollService: setStrategy(strategy)
Client->>PayrollService: calculateSalary(employee)
PayrollService->>ConcreteStrategy: calculate(employee)
ConcreteStrategy-->>PayrollService: returns salary
PayrollService-->>Client: returns salary
When to Use the Strategy Pattern?
Use the Strategy pattern when:
- You want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime.
- You have a lot of similar classes that only differ in the way they execute some behavior.
- You need to isolate the business logic of a class from the implementation details of its algorithms. That way, the logic isn’t cluttered with implementation details.
- Your class has a massive conditional statement that switches between different variants of the same algorithm.
[!TIP] Strategy Pattern and Dependency Injection
In modern applications using frameworks like Spring, you often don’t need a manual factory. You can define each strategy as a component (e.g., with
@Service) and inject aMap<EmployeeType, ISalaryCalculationStrategy>. The framework automatically populates the map, and you can retrieve the correct strategy withmap.get(employee.getType()). This is an even more powerful and decoupled approach!
Conclusion
The Strategy Pattern is a powerful tool for transforming complex, conditional logic into clean, maintainable, and extensible code. By encapsulating algorithms into separate, interchangeable classes, you adhere to core SOLID principles and build systems that are prepared for future changes. The next time you find yourself writing a long if-else or switch statement, ask yourself if the Strategy Pattern could be your ticket to a cleaner design.
Quiz: Test Your Knowledge
Question: What is the primary difference between the Strategy pattern and the State pattern?
Answer: Both patterns are structurally similar, using a context and interchangeable objects to change behavior. However, their intent is different.
- The Strategy pattern is about having different algorithms to accomplish the same goal, and the client is often responsible for choosing and providing the strategy to the context.
- The State pattern is about altering an object's behavior when its internal state changes. The transitions between states are typically managed by the context or the state objects themselves, not by the client.