Strategy and Template Method Patterns Explained

Introduction
In the previous post, you learned two structural patterns that wrap objects: Decorator adds behavior, Proxy controls access. Now we enter Phase 5: Behavioral Patterns — patterns that deal with how objects communicate and assign responsibilities.
This post covers two classic behavioral patterns that tame algorithmic complexity:
| Pattern | One-Line Summary |
|---|---|
| Strategy | Encapsulates a family of algorithms, makes them interchangeable at runtime |
| Template Method | Defines the skeleton of an algorithm, lets subclasses fill in the details |
Both patterns are about algorithms — but they tackle the problem from opposite directions. Strategy selects which algorithm to use. Template Method defines how an algorithm is structured.
What You'll Learn
✅ Replace tangled if/else and switch chains with the Strategy pattern
✅ Select and swap algorithms at runtime without changing client code
✅ Define algorithm skeletons with the Template Method pattern
✅ Use hook methods to give subclasses optional override points
✅ Implement both patterns in TypeScript, Python, and Java
✅ Know when to use Strategy vs Template Method — and when to combine them
Prerequisites
- Completed Decorator and Proxy Patterns
- Familiar with Polymorphism and Interfaces
- Comfortable with Inheritance and Composition
Part 1: The Strategy Pattern
The Problem: Conditional Logic Explosion
You're building a navigation app. Depending on the transport mode, you calculate routes differently:
// ❌ Before Strategy — growing conditional logic
class Navigator {
buildRoute(origin: string, destination: string, mode: string): void {
if (mode === "car") {
console.log(`Building car route: fastest roads from ${origin} to ${destination}`);
// ... complex car routing logic
} else if (mode === "bike") {
console.log(`Building bike route: bike lanes from ${origin} to ${destination}`);
// ... complex bike routing logic
} else if (mode === "walk") {
console.log(`Building walking route: pedestrian paths from ${origin} to ${destination}`);
// ... complex walking logic
} else if (mode === "transit") {
console.log(`Building transit route: buses and trains from ${origin} to ${destination}`);
// ... complex transit logic
}
// Every new transport mode = another else-if branch
}
}This class has four problems:
- It violates the Open/Closed Principle — you must modify it to add new transport modes
- Every new mode inflates the method with more conditional branches
- Unit testing requires testing all branches together — not in isolation
- The routing logic can't be reused elsewhere independently
The Strategy pattern solves this: extract each algorithm variant into its own class, make them all implement a common interface, and let the client pick which one to use.
Pattern Structure
Participants:
- Strategy — the interface all algorithm variants implement
- ConcreteStrategy — a specific algorithm (e.g.,
CarRouteStrategy,BikeRouteStrategy) - Context — holds a reference to the current strategy and delegates algorithm execution to it
Key insight: The Context doesn't care which concrete strategy it holds — it just calls execute(). This means you can swap strategies at runtime without changing any Context code.
TypeScript Implementation
Example 1: Navigation Routes
// Strategy interface — the contract all routing algorithms must fulfill
interface RouteStrategy {
buildRoute(origin: string, destination: string): string;
}
// Concrete strategies — each encapsulates one routing algorithm
class CarRouteStrategy implements RouteStrategy {
buildRoute(origin: string, destination: string): string {
return `🚗 Car route: Fastest roads from ${origin} to ${destination} (avoids tolls)`;
}
}
class BikeRouteStrategy implements RouteStrategy {
buildRoute(origin: string, destination: string): string {
return `🚴 Bike route: Dedicated bike lanes from ${origin} to ${destination}`;
}
}
class WalkingRouteStrategy implements RouteStrategy {
buildRoute(origin: string, destination: string): string {
return `🚶 Walking route: Pedestrian paths from ${origin} to ${destination}`;
}
}
class TransitRouteStrategy implements RouteStrategy {
buildRoute(origin: string, destination: string): string {
return `🚌 Transit route: Bus + Metro from ${origin} to ${destination}`;
}
}
// Context — uses whichever strategy the client provides
class Navigator {
private strategy: RouteStrategy;
constructor(strategy: RouteStrategy) {
this.strategy = strategy;
}
// Swap strategy at any time — open/closed
setStrategy(strategy: RouteStrategy): void {
this.strategy = strategy;
}
navigate(origin: string, destination: string): void {
const route = this.strategy.buildRoute(origin, destination);
console.log(route);
}
}
// Usage — choose algorithm at runtime
const nav = new Navigator(new CarRouteStrategy());
nav.navigate("Home", "Office");
// 🚗 Car route: Fastest roads from Home to Office (avoids tolls)
nav.setStrategy(new BikeRouteStrategy());
nav.navigate("Home", "Office");
// 🚴 Bike route: Dedicated bike lanes from Home to Office
nav.setStrategy(new TransitRouteStrategy());
nav.navigate("Home", "Airport");
// 🚌 Transit route: Bus + Metro from Home to AirportNo if/else anywhere in Navigator. Adding a new transport mode is just a new class that implements RouteStrategy — the Navigator stays closed to modification.
Example 2: Payment Processing
A real-world scenario — e-commerce checkout with multiple payment methods:
interface PaymentStrategy {
pay(amount: number, details: Record<string, string>): boolean;
}
class CreditCardStrategy implements PaymentStrategy {
pay(amount: number, details: Record<string, string>): boolean {
console.log(`💳 Processing $${amount} via Credit Card ending in ${details.cardNumber?.slice(-4)}`);
// ... credit card processing logic
return true;
}
}
class PayPalStrategy implements PaymentStrategy {
pay(amount: number, details: Record<string, string>): boolean {
console.log(`🅿️ Processing $${amount} via PayPal account ${details.email}`);
// ... PayPal API call
return true;
}
}
class CryptoStrategy implements PaymentStrategy {
pay(amount: number, details: Record<string, string>): boolean {
console.log(`₿ Processing $${amount} worth of ${details.currency} to wallet ${details.walletAddress}`);
// ... blockchain transaction
return true;
}
}
class BuyNowPayLaterStrategy implements PaymentStrategy {
pay(amount: number, details: Record<string, string>): boolean {
console.log(`📅 Split $${amount} into 4 installments via ${details.provider}`);
return true;
}
}
class ShoppingCart {
private items: { name: string; price: number }[] = [];
private paymentStrategy: PaymentStrategy;
constructor(paymentStrategy: PaymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
setPaymentStrategy(strategy: PaymentStrategy): void {
this.paymentStrategy = strategy;
}
addItem(name: string, price: number): void {
this.items.push({ name, price });
}
checkout(details: Record<string, string>): void {
const total = this.items.reduce((sum, item) => sum + item.price, 0);
console.log(`\nOrder total: $${total}`);
const success = this.paymentStrategy.pay(total, details);
if (success) {
console.log("✅ Payment successful!\n");
}
}
}
// Usage
const cart = new ShoppingCart(new CreditCardStrategy());
cart.addItem("Laptop", 999);
cart.addItem("Mouse", 49);
cart.checkout({ cardNumber: "4111111111112345" });
// User switches to PayPal at checkout
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout({ email: "alice@example.com" });Example 3: Sort Strategy
interface SortStrategy<T> {
sort(data: T[]): T[];
}
class BubbleSortStrategy<T> implements SortStrategy<T> {
sort(data: T[]): T[] {
const arr = [...data];
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSortStrategy<T> implements SortStrategy<T> {
sort(data: T[]): T[] {
if (data.length <= 1) return data;
const pivot = data[Math.floor(data.length / 2)];
const left = data.filter(x => x < pivot);
const middle = data.filter(x => x === pivot);
const right = data.filter(x => x > pivot);
return [...this.sort(left), ...middle, ...this.sort(right)];
}
}
class Sorter<T> {
constructor(private strategy: SortStrategy<T>) {}
setStrategy(strategy: SortStrategy<T>): void {
this.strategy = strategy;
}
sort(data: T[]): T[] {
return this.strategy.sort(data);
}
}
// Choose algorithm based on dataset size
const sorter = new Sorter<number>(new BubbleSortStrategy());
const smallDataset = [5, 2, 8, 1, 9];
console.log(sorter.sort(smallDataset)); // [1, 2, 5, 8, 9]
sorter.setStrategy(new QuickSortStrategy());
const largeDataset = Array.from({ length: 1000 }, () => Math.random() * 1000);
sorter.sort(largeDataset); // QuickSort handles large datasets efficientlyPython Implementation
Python's first-class functions let you implement Strategy even more concisely — either with classes or with plain functions:
from abc import ABC, abstractmethod
from typing import Callable
# --- Class-based Strategy (same as TypeScript) ---
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, filename: str) -> str:
pass
class ZipStrategy(CompressionStrategy):
def compress(self, filename: str) -> str:
return f"Compressing {filename} using ZIP format (good compression ratio)"
class RarStrategy(CompressionStrategy):
def compress(self, filename: str) -> str:
return f"Compressing {filename} using RAR format (smaller archives)"
class TarGzStrategy(CompressionStrategy):
def compress(self, filename: str) -> str:
return f"Compressing {filename} using TAR.GZ format (Unix/Linux standard)"
class Archiver:
def __init__(self, strategy: CompressionStrategy):
self._strategy = strategy
def set_strategy(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
def archive(self, filename: str) -> str:
return self._strategy.compress(filename)
# Class-based usage
archiver = Archiver(ZipStrategy())
print(archiver.archive("documents.tar"))
# Compressing documents.tar using ZIP format (good compression ratio)
archiver.set_strategy(TarGzStrategy())
print(archiver.archive("backup.tar"))
# Compressing backup.tar using TAR.GZ format (Unix/Linux standard)
# --- Function-based Strategy (Pythonic shortcut) ---
# When the algorithm is simple, use callables directly
StrategyFn = Callable[[str], str]
def zip_compress(filename: str) -> str:
return f"ZIP: {filename}.zip"
def rar_compress(filename: str) -> str:
return f"RAR: {filename}.rar"
class FunctionalArchiver:
def __init__(self, strategy: StrategyFn):
self._strategy = strategy
def archive(self, filename: str) -> str:
return self._strategy(filename)
fa = FunctionalArchiver(zip_compress)
print(fa.archive("report")) # ZIP: report.zip
fa._strategy = rar_compress
print(fa.archive("report")) # RAR: report.rar
# Even simpler: use a lambda
fa._strategy = lambda f: f"GZIP: {f}.gz"
print(fa.archive("report")) # GZIP: report.gzJava Implementation
// Strategy Pattern — Discount calculation in an e-commerce system
// Strategy interface
public interface DiscountStrategy {
double calculateDiscount(double originalPrice);
String getDescription();
}
// Concrete strategies
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return 0;
}
@Override
public String getDescription() {
return "No discount applied";
}
}
public class PercentageDiscountStrategy implements DiscountStrategy {
private final double percentage;
public PercentageDiscountStrategy(double percentage) {
this.percentage = percentage;
}
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice * (percentage / 100);
}
@Override
public String getDescription() {
return String.format("%.0f%% off", percentage);
}
}
public class FixedAmountDiscountStrategy implements DiscountStrategy {
private final double amount;
public FixedAmountDiscountStrategy(double amount) {
this.amount = amount;
}
@Override
public double calculateDiscount(double originalPrice) {
return Math.min(amount, originalPrice);
}
@Override
public String getDescription() {
return String.format("$%.2f off", amount);
}
}
public class BuyOneGetOneFreeStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
return originalPrice / 2; // 50% effective discount
}
@Override
public String getDescription() {
return "Buy 1, Get 1 Free";
}
}
// Context
public class PricingEngine {
private DiscountStrategy strategy;
public PricingEngine(DiscountStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double getFinalPrice(double originalPrice) {
double discount = strategy.calculateDiscount(originalPrice);
double finalPrice = originalPrice - discount;
System.out.printf("Original: $%.2f | %s | Final: $%.2f%n",
originalPrice, strategy.getDescription(), finalPrice);
return finalPrice;
}
}
// Usage
public class Main {
public static void main(String[] args) {
PricingEngine engine = new PricingEngine(new NoDiscountStrategy());
engine.getFinalPrice(100.0);
// Original: $100.00 | No discount applied | Final: $100.00
engine.setStrategy(new PercentageDiscountStrategy(20));
engine.getFinalPrice(100.0);
// Original: $100.00 | 20% off | Final: $80.00
engine.setStrategy(new FixedAmountDiscountStrategy(15));
engine.getFinalPrice(100.0);
// Original: $100.00 | $15.00 off | Final: $85.00
engine.setStrategy(new BuyOneGetOneFreeStrategy());
engine.getFinalPrice(100.0);
// Original: $100.00 | Buy 1, Get 1 Free | Final: $50.00
}
}Part 2: The Template Method Pattern
The Problem: Algorithm Duplication
You're building a data mining application. It reads files in different formats (CSV, JSON, XML) and generates reports. The overall process is always the same:
1. Open file → 2. Extract data → 3. Parse data → 4. Analyze data → 5. Generate report → 6. Close fileBut steps 2 and 3 differ per format. Without Template Method, you'd have three classes that each copy-paste the same structure:
// ❌ Duplication — identical structure, different inner steps
class CSVDataMiner {
mine(filePath: string): void {
this.openFile(filePath); // same
this.extractCSVData(); // different
this.parseCSVData(); // different
this.analyzeData(); // same
this.generateReport(); // same
this.closeFile(); // same
}
// ...
}
class JSONDataMiner {
mine(filePath: string): void {
this.openFile(filePath); // same (duplicate)
this.extractJSONData(); // different
this.parseJSONData(); // different
this.analyzeData(); // same (duplicate)
this.generateReport(); // same (duplicate)
this.closeFile(); // same (duplicate)
}
// ...
}Four out of six steps are identical — but you're duplicating them in every class. If analyzeData ever changes, you update three places.
The Template Method pattern solves this: put the algorithm skeleton in a base class and let subclasses override only the steps that vary.
Pattern Structure
Participants:
- AbstractClass — defines the
templateMethod()which calls all steps in order; implements the invariant steps; declares abstract methods for variant steps; optionally provides hook methods - ConcreteClass — implements the abstract steps that vary; may optionally override hook methods
Two types of methods in the base class:
- Abstract methods — must be overridden in subclasses (the variable parts)
- Hook methods — have a default (empty or minimal) implementation; subclasses may override them (optional customization points)
TypeScript Implementation
Example 1: Data Mining Pipeline
// Abstract base — defines the algorithm skeleton
abstract class DataMiner {
// The template method — final: subclasses cannot change the order
public mine(filePath: string): void {
this.openFile(filePath); // invariant: same for all formats
this.extractData(); // variant: must be overridden
this.parseData(); // variant: must be overridden
this.analyzeData(); // invariant: same for all
this.generateReport(); // invariant: same for all
this.closeFile(); // invariant: same for all
}
// Invariant steps — implemented here, not overridden
private openFile(filePath: string): void {
console.log(`📂 Opening file: ${filePath}`);
}
private analyzeData(): void {
console.log("📊 Analyzing extracted data...");
}
private generateReport(): void {
console.log("📄 Generating report...");
}
private closeFile(): void {
console.log("🔒 Closing file.");
}
// Variant steps — subclasses MUST implement these
protected abstract extractData(): void;
protected abstract parseData(): void;
// Hook method — optional override, empty default
protected afterParse(): void {
// Subclasses can override to add post-parse behavior
}
}
// Concrete class — only implements what varies
class CSVDataMiner extends DataMiner {
protected extractData(): void {
console.log("📋 Extracting rows from CSV file");
}
protected parseData(): void {
console.log("🔍 Parsing CSV: splitting by commas, mapping headers");
}
}
class JSONDataMiner extends DataMiner {
protected extractData(): void {
console.log("📋 Extracting JSON objects from file");
}
protected parseData(): void {
console.log("🔍 Parsing JSON: deserializing nested objects");
}
// This subclass uses the optional hook
protected afterParse(): void {
console.log("🔗 JSON: Resolving $ref references...");
}
}
class XMLDataMiner extends DataMiner {
protected extractData(): void {
console.log("📋 Extracting XML nodes from file");
}
protected parseData(): void {
console.log("🔍 Parsing XML: building DOM tree");
}
}
// Usage — same interface, different inner behavior
console.log("=== CSV Mining ===");
new CSVDataMiner().mine("sales_2026.csv");
console.log("\n=== JSON Mining ===");
new JSONDataMiner().mine("products.json");
console.log("\n=== XML Mining ===");
new XMLDataMiner().mine("config.xml");Output:
=== CSV Mining ===
📂 Opening file: sales_2026.csv
📋 Extracting rows from CSV file
🔍 Parsing CSV: splitting by commas, mapping headers
📊 Analyzing extracted data...
📄 Generating report...
🔒 Closing file.
=== JSON Mining ===
📂 Opening file: products.json
📋 Extracting JSON objects from file
🔍 Parsing JSON: deserializing nested objects
📊 Analyzing extracted data...
📄 Generating report...
🔒 Closing file.Example 2: Beverage Preparation
The classic Gang of Four example — making tea vs. coffee:
abstract class HotBeverage {
// Template method — the fixed recipe
public prepare(): void {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) { // hook controls optional step
this.addCondiments();
}
}
// Fixed steps
private boilWater(): void {
console.log("🔥 Boiling water");
}
private pourInCup(): void {
console.log("☕ Pouring into cup");
}
// Variant steps — subclasses fill in
protected abstract brew(): void;
protected abstract addCondiments(): void;
// Hook — subclasses can override to say "no condiments, please"
protected customerWantsCondiments(): boolean {
return true; // default: yes
}
}
class Tea extends HotBeverage {
protected brew(): void {
console.log("🍃 Steeping the tea bag (3 minutes)");
}
protected addCondiments(): void {
console.log("🍋 Adding lemon slice");
}
}
class Coffee extends HotBeverage {
protected brew(): void {
console.log("☕ Dripping coffee through filter");
}
protected addCondiments(): void {
console.log("🥛 Adding sugar and milk");
}
}
class BlackCoffee extends HotBeverage {
protected brew(): void {
console.log("☕ Dripping coffee through filter");
}
protected addCondiments(): void {
// Never called anyway
}
// Override hook — no condiments for black coffee
protected customerWantsCondiments(): boolean {
return false;
}
}
console.log("--- Making Tea ---");
new Tea().prepare();
console.log("\n--- Making Coffee ---");
new Coffee().prepare();
console.log("\n--- Making Black Coffee ---");
new BlackCoffee().prepare();
// No "Adding" step — hook returned falseExample 3: Order Processing Workflow
abstract class OrderProcessor {
// Template method — processing pipeline
public processOrder(orderId: string): void {
console.log(`\n🛒 Processing order #${orderId}`);
if (!this.validateOrder(orderId)) {
console.log("❌ Order validation failed. Aborting.");
return;
}
this.processPayment(orderId);
this.fulfillOrder(orderId);
this.sendNotification(orderId);
this.logOrder(orderId); // invariant hook behavior
}
// Abstract — each channel processes differently
protected abstract validateOrder(orderId: string): boolean;
protected abstract processPayment(orderId: string): void;
protected abstract fulfillOrder(orderId: string): void;
// Invariant steps (same for all channels)
private sendNotification(orderId: string): void {
console.log(`📧 Confirmation email sent for order #${orderId}`);
}
private logOrder(orderId: string): void {
console.log(`📝 Order #${orderId} logged to audit trail`);
}
}
class OnlineOrderProcessor extends OrderProcessor {
protected validateOrder(orderId: string): boolean {
console.log("🔍 Validating online order: checking inventory + fraud detection");
return true;
}
protected processPayment(orderId: string): void {
console.log("💳 Processing online payment via Stripe");
}
protected fulfillOrder(orderId: string): void {
console.log("📦 Creating shipment label, scheduling courier pickup");
}
}
class InStoreOrderProcessor extends OrderProcessor {
protected validateOrder(orderId: string): boolean {
console.log("🔍 Validating in-store order: checking shelf availability");
return true;
}
protected processPayment(orderId: string): void {
console.log("🏪 Processing POS terminal payment");
}
protected fulfillOrder(orderId: string): void {
console.log("🛍️ Order ready for immediate pickup");
}
}
new OnlineOrderProcessor().processOrder("ORD-1001");
new InStoreOrderProcessor().processOrder("ORD-1002");Python Implementation
from abc import ABC, abstractmethod
class ReportGenerator(ABC):
"""Abstract base defining the report generation algorithm."""
# Template method — sealed (Python doesn't enforce, but conventionally final)
def generate(self, data: dict) -> str:
header = self._create_header(data)
body = self._create_body(data)
footer = self._create_footer()
extras = self._add_extras(data) # hook
return "\n".join(filter(None, [header, body, extras, footer]))
# Invariant steps
def _create_footer(self) -> str:
return "--- End of Report ---"
# Abstract steps — subclasses must implement
@abstractmethod
def _create_header(self, data: dict) -> str:
pass
@abstractmethod
def _create_body(self, data: dict) -> str:
pass
# Hook — optional enhancement
def _add_extras(self, data: dict) -> str:
return "" # Default: nothing extra
class PDFReportGenerator(ReportGenerator):
def _create_header(self, data: dict) -> str:
return f"[PDF] REPORT: {data['title'].upper()}\nGenerated: {data['date']}"
def _create_body(self, data: dict) -> str:
lines = [f" • {k}: {v}" for k, v in data.get("metrics", {}).items()]
return "[PDF Body]\n" + "\n".join(lines)
def _add_extras(self, data: dict) -> str:
return "[PDF] Embedded charts and graphs attached"
class HTMLReportGenerator(ReportGenerator):
def _create_header(self, data: dict) -> str:
return f"<h1>{data['title']}</h1><p>Date: {data['date']}</p>"
def _create_body(self, data: dict) -> str:
rows = "".join(f"<tr><td>{k}</td><td>{v}</td></tr>"
for k, v in data.get("metrics", {}).items())
return f"<table>{rows}</table>"
class MarkdownReportGenerator(ReportGenerator):
def _create_header(self, data: dict) -> str:
return f"# {data['title']}\n**Date:** {data['date']}"
def _create_body(self, data: dict) -> str:
lines = [f"| {k} | {v} |" for k, v in data.get("metrics", {}).items()]
return "| Metric | Value |\n|--------|-------|\n" + "\n".join(lines)
# Usage — same template, different output formats
report_data = {
"title": "Monthly Sales Report",
"date": "2026-03-05",
"metrics": {
"Revenue": "$45,200",
"Orders": 312,
"New Customers": 87,
}
}
generators = [PDFReportGenerator(), HTMLReportGenerator(), MarkdownReportGenerator()]
for generator in generators:
print(f"\n{'='*40}")
print(generator.generate(report_data))Java Implementation
// Template Method — Game lifecycle skeleton
public abstract class Game {
// Template method — final ensures subclasses can't change game loop order
public final void play() {
initialize();
startPlay();
while (!isDone()) {
takeTurn();
}
endPlay();
announceWinner();
}
// Abstract steps — subclasses must implement game-specific behavior
protected abstract void initialize();
protected abstract void startPlay();
protected abstract void takeTurn();
protected abstract boolean isDone();
protected abstract void endPlay();
// Invariant step — announcing winner is always the same structure
private void announceWinner() {
System.out.println("🏆 Game over! " + getWinnerName() + " wins!");
}
// Hook — subclass provides winner name
protected String getWinnerName() {
return "Player 1"; // default
}
}
public class Chess extends Game {
private int turnCount = 0;
@Override
protected void initialize() {
System.out.println("♟️ Chess: Setting up the board with 32 pieces");
}
@Override
protected void startPlay() {
System.out.println("♟️ Chess: White moves first");
}
@Override
protected void takeTurn() {
turnCount++;
System.out.println("♟️ Chess: Turn " + turnCount + " — player makes a move");
}
@Override
protected boolean isDone() {
return turnCount >= 3; // simplified: 3 turns for demo
}
@Override
protected void endPlay() {
System.out.println("♟️ Chess: Checkmate detected");
}
@Override
protected String getWinnerName() {
return "White Player";
}
}
public class TicTacToe extends Game {
private int moveCount = 0;
@Override
protected void initialize() {
System.out.println("❌ TicTacToe: Drawing 3x3 grid");
}
@Override
protected void startPlay() {
System.out.println("❌ TicTacToe: X goes first");
}
@Override
protected void takeTurn() {
moveCount++;
System.out.println("❌ TicTacToe: Move " + moveCount);
}
@Override
protected boolean isDone() {
return moveCount >= 3;
}
@Override
protected void endPlay() {
System.out.println("❌ TicTacToe: Three in a row!");
}
}
// Main
Game chess = new Chess();
chess.play();
System.out.println();
Game ttt = new TicTacToe();
ttt.play();Strategy vs Template Method — Side by Side
These two patterns both deal with algorithms, but they operate differently:
| Dimension | Strategy | Template Method |
|---|---|---|
| Mechanism | Object composition (holds a strategy object) | Class inheritance (extends the base class) |
| Algorithm selection | At runtime — swap the strategy object | At compile time — chosen by subclass |
| Varies by | The entire algorithm | Parts (steps) of the algorithm |
| Control flow | Client drives it — calls context.execute() | Base class drives it — calls template method |
| Open/Closed | Add algorithms by adding new strategy classes | Add variants by creating new subclasses |
| Flexibility | More flexible — strategy can change at runtime | Less flexible — locked to one subclass |
| Hollywood Principle | No — context calls strategy | Yes — "Don't call us, we'll call you" |
| Best for | Different algorithms with same intent | Same algorithm flow, different step details |
The key question: Can you swap the behavior at runtime after the object is created?
- Yes → Strategy (inject a different strategy object)
- No, it's baked in at class selection time → Template Method (use a different subclass)
Combining Strategy with Template Method
In real systems, these patterns are often used together. The Template Method defines the overall workflow; individual steps delegate to Strategy objects for fine-grained algorithm selection:
// Template Method defines the order
// Strategy handles the variable sub-algorithms
interface SortAlgorithm {
sort<T>(data: T[]): T[];
}
abstract class DataProcessor {
constructor(protected sortStrategy: SortAlgorithm) {}
// Template method — processing pipeline
public process(data: number[]): void {
const validated = this.validate(data); // abstract
const sorted = this.sortStrategy.sort(validated); // Strategy!
const result = this.transform(sorted); // abstract
this.output(result); // invariant
}
protected abstract validate(data: number[]): number[];
protected abstract transform(data: number[]): number[];
private output(data: number[]): void {
console.log("Result:", data.join(", "));
}
}
class FilterProcessor extends DataProcessor {
protected validate(data: number[]): number[] {
return data.filter(n => n > 0); // strip negatives
}
protected transform(data: number[]): number[] {
return data.map(n => n * 2); // double each value
}
}
// Usage
const processor = new FilterProcessor(new QuickSortStrategy());
processor.process([5, -1, 3, 0, 8, -2, 1]);
// Result: 2, 6, 10, 16Real-World Use Cases
Where You'll Find Strategy in the Wild
Authentication systems:
interface AuthStrategy {
authenticate(credentials: Record<string, string>): Promise<boolean>;
}
// JWTStrategy, OAuth2Strategy, APIKeyStrategy, BasicAuthStrategyLogging backends:
interface LogStrategy {
log(level: string, message: string): void;
}
// ConsoleLogStrategy, FileLogStrategy, ElasticSearchStrategy, DatadogStrategyCaching policies:
interface CacheStrategy {
get(key: string): any;
set(key: string, value: any, ttl: number): void;
}
// InMemoryCache, RedisCache, NoOpCache (for testing)File storage:
interface StorageStrategy {
upload(file: Buffer, path: string): Promise<string>;
}
// LocalStorageStrategy, S3Strategy, AzureBlobStrategy, GCSStrategyWhere You'll Find Template Method in the Wild
Spring Framework — AbstractController, JdbcTemplate, HibernateTemplate all use Template Method. The framework defines the workflow (transaction begin → execute → commit/rollback), you override only the query/update step.
JUnit test lifecycle — @BeforeEach, @Test, @AfterEach is a Template Method: the test runner drives the setup → test → teardown sequence.
React component lifecycle — componentDidMount, componentDidUpdate, componentWillUnmount hooks are Template Method: React drives the lifecycle, you override what you need.
Angular HTTP Interceptors — define a template: each request goes through intercept → transform → next.handle. You implement the transform step.
Common Pitfalls
Strategy Pitfalls
Pitfall 1: Bloating the strategy interface
// ❌ Forcing all strategies to implement everything
interface OverloadedStrategy {
sort(data: any[]): any[];
validate(data: any[]): boolean;
transform(data: any[]): any[];
// ...
}
// ✅ Keep strategy interface focused on ONE algorithm
interface SortStrategy {
sort<T>(data: T[]): T[];
}Pitfall 2: Using Strategy when a simple function suffices
// ❌ Overkill for simple one-method operations
class AddTaxStrategy implements PriceStrategy {
calculate(price: number): number { return price * 1.1; }
}
// ✅ Just pass a function
function applyPricing(price: number, strategy: (p: number) => number) {
return strategy(price);
}
applyPricing(100, price => price * 1.1);Pitfall 3: Allowing null strategies
// ❌ Crashes at runtime if strategy not set
class Context {
private strategy?: Strategy; // Could be undefined!
execute() { this.strategy!.run(); }
}
// ✅ Use Null Object Pattern or enforce via constructor
class Context {
constructor(private strategy: Strategy) {} // Required at construction
execute() { this.strategy.run(); }
}Template Method Pitfalls
Pitfall 1: Exposing too many abstract methods
// ❌ Too many abstract steps = too much burden on subclasses
abstract class Bloated {
template() { this.a(); this.b(); this.c(); this.d(); this.e(); }
abstract a(): void;
abstract b(): void;
abstract c(): void;
abstract d(): void;
abstract e(): void;
}
// ✅ Keep abstract methods minimal; make others hooks with defaults
abstract class Lean {
template() { this.a(); this.b(); this.c(); }
abstract a(): void; // must override
protected b(): void {} // optional hook
private c(): void { console.log("invariant"); } // always same
}Pitfall 2: Calling abstract methods in the constructor
// ❌ Dangerous — subclass fields not initialized yet in Java/TypeScript
abstract class Bad {
constructor() {
this.init(); // Subclass init() may reference uninitialized fields!
}
abstract init(): void;
}
// ✅ Call abstract methods from the template method, not constructor
abstract class Good {
public run(): void {
this.init(); // Called after construction — safe
this.execute();
}
protected abstract init(): void;
protected abstract execute(): void;
}Pitfall 3: Overriding the template method itself
The whole point of Template Method is that the skeleton is invariant. If subclasses override the template method, they break the contract.
// ✅ Mark template methods as final (Java) or use naming convention (TypeScript)
abstract class AbstractProcessor {
// "final" in Java; in TypeScript, use convention + linting
public readonly process = (data: string): void => {
this.preProcess(data);
this.execute(data);
this.postProcess(data);
};
protected abstract execute(data: string): void;
protected preProcess(data: string): void {} // hook
protected postProcess(data: string): void {} // hook
}Summary and Key Takeaways
Both Strategy and Template Method are behavioral patterns for managing algorithmic variation — but each has its place.
Strategy Pattern — Composition-based algorithm selection:
✅ Extracts algorithm variants into separate classes behind a shared interface
✅ Enables runtime algorithm swapping without touching the context
✅ Eliminates conditional branching (if/else, switch on type)
✅ Each strategy is independently testable
✅ New algorithms added without modifying existing code (Open/Closed)
Template Method Pattern — Inheritance-based algorithm skeleton:
✅ Defines the algorithm structure once in the base class
✅ Subclasses override only the variant steps — no duplication
✅ Hook methods provide optional customization points
✅ The Hollywood Principle: base class drives the flow, subclasses fill in details
✅ Guarantees the algorithm executes in the correct order
Decision guide:
- Will you need to change the algorithm at runtime? → Strategy
- Is the algorithm structure fixed, only inner steps vary? → Template Method
- Need both? → Combine them: Template Method for the skeleton, Strategy for the variable steps
What's Next
We have one post left in this series — the behavioral patterns trifecta:
➡️ Next: Observer, Command, and State Patterns — event-driven behavior, undo/redo, and state machines. After that, the OOP & Design Patterns series is complete!
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.