Module 4 · 4. Behavioral Patterns and Interview Practice · Lesson 9 of 12
Strategy and Template Method
Learning outcome
By the end of this lesson you will be able to:
- Distinguish when the algorithm's structure is fixed (Template Method) versus when the implementation must be changeable at runtime (Strategy).
- Implement testable, production-style C# strategies and a template-method processor that highlight the trade-offs.
- Recognize misuse signals (fragile base class, over-subclassing, needless runtime indirection) and choose the simpler alternative when appropriate.
Intuition
Template Method and Strategy both let you vary parts of an algorithm without modifying the algorithm's clients. Their difference is primary: who controls the algorithm structure?
- Template Method: The algorithm skeleton is owned by an abstract superclass; subclasses override specific steps. Good when the sequence of steps is fixed, and only some steps vary.
- Strategy: The algorithm's variable part is extracted into an object (a strategy) that can be swapped in. Good when clients need to swap behaviors at runtime, or when you want to avoid subclass explosion.
Think: "Do I need to swap an implementation while the application runs? Use Strategy. Is the orchestration fixed and only the steps vary? Use Template Method."
Deep dive
Forces to weigh:
- Runtime variability vs compile-time/type variability
- Test isolation and mocking: strategies are naturally injected and mockable
- Number of variations: many orthogonal variations favor strategies/composition over subclassing
- Encapsulation and cohesion: template method centralizes the procedure, strategies isolate details
Minimal pseudocode for Template Method (fixed algorithm skeleton):
Minimal pseudocode for Strategy (runtime-swappable behavior):
Trade-offs summary:
- Template Method gives stronger control over the sequence and can prevent invalid sequences; but it makes behavior changes require new subclasses (or conditional logic in base class).
- Strategy is more flexible and composes behaviors; but it may require the host to coordinate steps that were previously centralized.
Failure modes
- Template Method overuse: creating many tiny subclasses just to tweak one line leads to subclass explosion.
- Fragile base class problem: changing the base class template breaks subclasses unexpectedly.
- Strategy overuse: extracting every small variation into a strategy results in unnecessary indirection and harder debugging.
- Mixing both incorrectly: using template method but also injecting strategies for the same variation can confuse ownership of responsibilities.
Misuse signals:
- If you need to support a new variation at runtime, Template Method is a smell.
- If the template method base class has many protected hooks used only by one subclass, composition (strategy or helper objects) is likely better.
Interview drill
Question: "You have an order-processing pipeline; payment methods change often at runtime while the step order is fixed. Which pattern and why?"
Good answer checklist:
- Mention runtime variability -> Strategy
- Mention fixed orchestration -> keep a coordinator or template-like procedure
- Note testability: inject payment strategies and unit-test them in isolation
- Discuss trade-offs: runtime flexibility vs simpler code
Micro-tests / short whiteboard tasks:
1) Draw class diagram for Template Method when adding two new payment methods. How many new types? 2) Explain how you'd swap a payment method in Strategy at runtime in a DI-enabled application.
Expected micro-test answers are in the checklist above.
Revision checklist
- Is the sequence of steps invariant? Consider Template Method.
- Do you need to swap behaviors at runtime or swap per request/session? Consider Strategy.
- Can you write small, focused interfaces for strategies so they are single-responsibility and mockable?
- Avoid protected state in template base classes; prefer passing context objects where possible.
Production code
Guidance for production readiness:
- Prefer explicit constructor injection for strategies so DI frameworks can manage lifetimes and composition.
- Keep template base classes small and well-documented; mark the template method as sealed if your language allows to prevent accidental override of the orchestration.
- Use small, well-named interfaces for strategies; if there are many orthogonal variations, prefer composition of multiple strategies rather than one bulky strategy interface.
- Add telemetry and structured logs at key steps; avoid side-effects in virtual methods that are hard to observe.
Warning (misuse): don't make the base class a God object. If many protected members are needed by subclasses, refactor into helper objects or injected strategies.
Code walkthrough
Below is a compact, deterministic example you can run. It demonstrates:
- A Template Method (the abstract order processor) where subclasses implement payment.
- A Strategy-based processor that accepts IPaymentStrategy and allows runtime swapping.
- Why Strategy is preferable when runtime swapping is required and why Template Method is preferable when the orchestration must be enforced.
The runnable example (in the attached Program.cs) prints deterministic lines so you can assert expected behavior in micro-tests.
Notes on production transitions:
- When converting a Template Method subclass to Strategy, extract the varying methods into a strategy and inject it into the processor. This reduces the number of subclasses.
- Keep both patterns small and use clear naming so code reviewers immediately understand ownership of the orchestration.
Exercises (with brief explanations):
1) Convert a template-method that has two hooks into a strategy-based composition where each hook becomes a separate injectable strategy. Why? Answer: When hooks vary independently, separate strategies reduce combinations and increase reuse.
2) Given an existing Strategy implementation used across modules, how would you add a cross-cutting concern (e.g., metrics) without changing each strategy? Answer: Wrap the strategy with a decorator implementing the same interface; this preserves runtime swapping and centralizes metrics.
Executable code examples
Template Method vs Strategy demo (Console)
Program.cs
using System;
// Deterministic demo illustrating Template Method and Strategy patterns.
// The example prints fixed lines so tests can assert expected behavior.
// Why prefer each pattern here:
// - Template Method preferable when the orchestration (sequence of steps) must be enforced centrally.
// - Strategy preferable when the payment implementation must be swapped at runtime.
// Misuse warning: Don't create many tiny subclasses just to tweak one step; prefer composition/strategies when variations multiply.
record Order(int Id);
// -------------------------
// Template Method variant
// -------------------------
abstract class OrderProcessorTemplate
{
// The template method: sealed to prevent changing orchestration in subclasses
public void Process(Order order)
{
Validate(order);
AuthorizePayment(order);
ReserveInventory(order);
NotifyCustomer(order);
}
protected abstract void Validate(Order order);
protected abstract void AuthorizePayment(Order order);
protected virtual void ReserveInventory(Order order)
{
Console.WriteLine($"Template: Reserve inventory for order {order.Id}");
}
protected virtual void NotifyCustomer(Order order)
{
Console.WriteLine($"Template: Notify customer for order {order.Id}");
}
}
// Concrete template subclass: credit card payment
class CreditCardOrderProcessor : OrderProcessorTemplate
{
protected override void Validate(Order order)
{
Console.WriteLine($"Template: Validate order {order.Id}");
}
protected override void AuthorizePayment(Order order)
{
Console.WriteLine($"Template: Authorize payment with credit card for order {order.Id}");
}
}
// -------------------------
// Strategy variant
// -------------------------
interface IPaymentStrategy
{
void Pay(Order order);
}
class CreditCardPaymentStrategy : IPaymentStrategy
{
public void Pay(Order order)
{
Console.WriteLine($"Strategy: Process payment with Credit Card for order {order.Id}");
}
}
class MockPaymentStrategy : IPaymentStrategy
{
public void Pay(Order order)
{
Console.WriteLine($"Strategy: Process payment with Mock Gateway for order {order.Id}");
}
}
class OrderProcessorWithStrategy
{
private IPaymentStrategy _payment;
public OrderProcessorWithStrategy(IPaymentStrategy payment)
{
_payment = payment ?? throw new ArgumentNullException(nameof(payment));
}
public void SetPaymentStrategy(IPaymentStrategy payment)
{
_payment = payment ?? throw new ArgumentNullException(nameof(payment));
}
public void Process(Order order)
{
Validate(order);
_payment.Pay(order);
ReserveInventory(order);
NotifyCustomer(order);
}
private void Validate(Order order)
{
Console.WriteLine($"Strategy: Validate order {order.Id}");
}
private void ReserveInventory(Order order)
{
Console.WriteLine($"Strategy: Reserve inventory for order {order.Id}");
}
private void NotifyCustomer(Order order)
{
Console.WriteLine($"Strategy: Notify customer for order {order.Id}");
}
}
// -------------------------
// Demo runner
// -------------------------
class Program
{
static void Main()
{
Console.WriteLine("Demo: Template Method vs Strategy (deterministic output)");
var order = new Order(42);
// Template Method demo
Console.WriteLine("Pattern: Template Method - using CreditCardOrderProcessor");
var templateProcessor = new CreditCardOrderProcessor();
templateProcessor.Process(order);
Console.WriteLine();
// Strategy demo with Credit Card
Console.WriteLine("Pattern: Strategy - using CreditCardPaymentStrategy");
var strategyProcessor = new OrderProcessorWithStrategy(new CreditCardPaymentStrategy());
strategyProcessor.Process(order);
Console.WriteLine();
// Swap payment strategy at runtime
Console.WriteLine("Pattern: Strategy - swapping to MockPaymentStrategy at runtime");
strategyProcessor.SetPaymentStrategy(new MockPaymentStrategy());
strategyProcessor.Process(order);
Console.WriteLine();
Console.WriteLine("Conclusion: Template Method fixes the orchestration; Strategy allows runtime swapping of payment implementations.");
}
}