Template Method Pattern
🎯 Explain Like I'm 5...
Imagine your morning routine! Every morning you follow the same steps, but some details change each day. Let's see how this works:
📝 The Morning Routine Template:
- • 1. Wake up (everyone does this the same way)
- • 2. Brush teeth (everyone does this the same way)
- • 3. Eat breakfast (DIFFERENT for everyone!)
- • 4. Get dressed (DIFFERENT for everyone!)
- • 5. Go to school/work (everyone does this)
💡 The Template Method Magic!
- • The ORDER of steps is FIXED (the template!) 📋
- • Some steps are the SAME for everyone (wake up, brush teeth)
- • Some steps are DIFFERENT for each person:
- • You might eat cereal 🥣
- • Your sister might eat toast 🍞
- • Your dad might eat eggs 🍳
- • But EVERYONE follows the same routine ORDER!
🌟 The Key Idea:
Template Method is like a RECIPE! The recipe tells you the STEPS (add flour, mix, bake) but YOU decide the DETAILS (chocolate chip or oatmeal cookies?). The skeleton stays the same, but you customize the parts!
🚀 Why Is This Pattern Useful?
- ✨Reuse the same algorithm structure in multiple places!
- ✨Let subclasses customize behavior without changing the overall flow!
- ✨Prevent code duplication - write common steps once!
- ✨Enforce a specific order of operations that can't be changed!
📋 Pattern Purpose
The Template Method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm's structure. It's a behavioral pattern that promotes code reuse and follows the Hollywood Principle: "Don't call us, we'll call you."
⚡ When to Use Template Method Pattern
- ✓You have multiple classes that implement similar algorithms with minor variations
- ✓You want to let subclasses extend only particular steps of an algorithm
- ✓You need to control the order of operations and prevent subclasses from changing it
- ✓You have common behavior that should be shared across multiple classes
- ✓You want to avoid code duplication in similar algorithms
🧩 Pattern Components
AbstractClass:
Defines the template method and declares abstract primitive operations
ConcreteClass:
Implements the primitive operations to carry out subclass-specific steps
Template Method:
Defines the skeleton of the algorithm, calling primitive operations in a specific order
Primitive Operations:
Abstract methods that subclasses must implement, or hooks that can be optionally overridden
💻 Java Implementations
Example 1: Beverage Template (Tea and Coffee)
Making different beverages with the same preparation steps but different ingredients.
// Abstract Class - defines the template methodpublic abstract class Beverage { // Template Method - defines the algorithm skeleton // FINAL to prevent subclasses from changing the flow public final void prepareBeverage() { boilWater(); // Common step brew(); // Varies by subclass pourInCup(); // Common step addCondiments(); // Varies by subclass // Optional hook - subclasses can override if needed if (customerWantsCondiments()) { addExtraCondiments(); } } // Common step - same for all beverages private void boilWater() { System.out.println("Boiling water..."); } // Common step - same for all beverages private void pourInCup() { System.out.println("Pouring into cup..."); } // Abstract methods - subclasses MUST implement protected abstract void brew(); protected abstract void addCondiments(); // Hook - optional, can be overridden protected void addExtraCondiments() { // Default: do nothing } // Hook - default implementation, can be overridden protected boolean customerWantsCondiments() { return true; }}// Concrete Class 1 - Tea implementationpublic class Tea extends Beverage { @Override protected void brew() { System.out.println("Steeping the tea bag..."); } @Override protected void addCondiments() { System.out.println("Adding lemon..."); } @Override protected void addExtraCondiments() { System.out.println("Adding honey..."); }}// Concrete Class 2 - Coffee implementationpublic class Coffee extends Beverage { @Override protected void brew() { System.out.println("Dripping coffee through filter..."); } @Override protected void addCondiments() { System.out.println("Adding sugar and milk..."); } @Override protected boolean customerWantsCondiments() { // Let's say coffee drinkers are picky String answer = getUserInput(); return answer.toLowerCase().startsWith("y"); } private String getUserInput() { System.out.print("Would you like milk and sugar? (y/n): "); // In real code, would read from console return "y"; // Simplified for example }}// Concrete Class 3 - Hot Chocolate implementationpublic class HotChocolate extends Beverage { @Override protected void brew() { System.out.println("Mixing hot chocolate powder..."); } @Override protected void addCondiments() { System.out.println("Adding whipped cream..."); } @Override protected void addExtraCondiments() { System.out.println("Adding marshmallows and chocolate chips..."); }}// Client codepublic class BeverageDemo { public static void main(String[] args) { System.out.println("Making tea..."); Beverage tea = new Tea(); tea.prepareBeverage(); System.out.println("\n" + "=".repeat(30) + "\n"); System.out.println("Making coffee..."); Beverage coffee = new Coffee(); coffee.prepareBeverage(); System.out.println("\n" + "=".repeat(30) + "\n"); System.out.println("Making hot chocolate..."); Beverage hotChocolate = new HotChocolate(); hotChocolate.prepareBeverage(); }}/* OUTPUT:Making tea...Boiling water...Steeping the tea bag...Pouring into cup...Adding lemon...Adding honey...==============================Making coffee...Boiling water...Dripping coffee through filter...Pouring into cup...Would you like milk and sugar? (y/n): Adding sugar and milk...==============================Making hot chocolate...Boiling water...Mixing hot chocolate powder...Pouring into cup...Adding whipped cream...Adding marshmallows and chocolate chips...*/Example 2: Data Processor Template (CSV and JSON)
Processing different data formats with the same algorithm structure.
// Abstract Class - defines data processing templatepublic abstract class DataProcessor { // Template Method - defines the algorithm structure public final void processData(String filePath) { openFile(filePath); extractData(); parseData(); validateData(); if (needsTransformation()) { transformData(); } saveData(); closeFile(); System.out.println("Processing complete!\n"); } // Common steps private void openFile(String filePath) { System.out.println("Opening file: " + filePath); } private void closeFile() { System.out.println("Closing file..."); } // Abstract methods - must be implemented protected abstract void extractData(); protected abstract void parseData(); protected abstract void saveData(); // Hook with default implementation protected void validateData() { System.out.println("Validating data..."); } // Hook with default implementation protected void transformData() { System.out.println("Transforming data..."); } // Hook - can be overridden protected boolean needsTransformation() { return true; }}// Concrete Class 1 - CSV processingpublic class CsvDataProcessor extends DataProcessor { @Override protected void extractData() { System.out.println("Extracting CSV data..."); } @Override protected void parseData() { System.out.println("Parsing CSV: splitting by comma delimiter"); System.out.println("Creating data rows from CSV lines"); } @Override protected void validateData() { System.out.println("Validating CSV: checking column count"); System.out.println("Validating CSV: verifying data types"); } @Override protected void transformData() { System.out.println("Transforming CSV: trimming whitespace"); System.out.println("Transforming CSV: normalizing dates"); } @Override protected void saveData() { System.out.println("Saving CSV data to database..."); }}// Concrete Class 2 - JSON processingpublic class JsonDataProcessor extends DataProcessor { @Override protected void extractData() { System.out.println("Extracting JSON data..."); } @Override protected void parseData() { System.out.println("Parsing JSON: building object tree"); System.out.println("Creating data objects from JSON"); } @Override protected void validateData() { System.out.println("Validating JSON: checking schema"); System.out.println("Validating JSON: verifying required fields"); } @Override protected boolean needsTransformation() { // JSON is already structured, minimal transformation needed return false; } @Override protected void saveData() { System.out.println("Saving JSON data to NoSQL database..."); }}// Concrete Class 3 - XML processingpublic class XmlDataProcessor extends DataProcessor { @Override protected void extractData() { System.out.println("Extracting XML data..."); } @Override protected void parseData() { System.out.println("Parsing XML: building DOM tree"); System.out.println("Navigating XML nodes and attributes"); } @Override protected void validateData() { System.out.println("Validating XML: checking against XSD schema"); System.out.println("Validating XML: verifying namespace"); } @Override protected void transformData() { System.out.println("Transforming XML: applying XSLT"); System.out.println("Transforming XML: flattening nested structures"); } @Override protected void saveData() { System.out.println("Saving XML data to SQL database..."); }}// Client codepublic class DataProcessorDemo { public static void main(String[] args) { System.out.println("=== Processing CSV File ==="); DataProcessor csvProcessor = new CsvDataProcessor(); csvProcessor.processData("data.csv"); System.out.println("=== Processing JSON File ==="); DataProcessor jsonProcessor = new JsonDataProcessor(); jsonProcessor.processData("data.json"); System.out.println("=== Processing XML File ==="); DataProcessor xmlProcessor = new XmlDataProcessor(); xmlProcessor.processData("data.xml"); }}Example 3: Game Template (Chess and Checkers)
Different games following the same game flow template.
// Abstract Class - defines game flow templatepublic abstract class Game { // Template Method - defines the game flow public final void play() { initializeGame(); startGame(); while (!isGameOver()) { makePlay(); if (shouldShowStatus()) { displayStatus(); } } endGame(); displayWinner(); } // Abstract methods - must be implemented protected abstract void initializeGame(); protected abstract void makePlay(); protected abstract boolean isGameOver(); protected abstract void displayWinner(); // Common method with default implementation protected void startGame() { System.out.println("Game started!"); } // Common method with default implementation protected void endGame() { System.out.println("Game ended!"); } // Hook - can be overridden protected void displayStatus() { System.out.println("Current game status..."); } // Hook - can be overridden protected boolean shouldShowStatus() { return true; }}// Concrete Class 1 - Chess gamepublic class Chess extends Game { private int currentPlayer = 1; private int movesCount = 0; private static final int MAX_MOVES = 6; // Simplified @Override protected void initializeGame() { System.out.println("Chess Game Initialized!"); System.out.println("Setting up chess board..."); System.out.println("Placing pieces in starting positions..."); currentPlayer = 1; movesCount = 0; } @Override protected void makePlay() { movesCount++; System.out.println("Player " + currentPlayer + " makes move #" + movesCount); System.out.println("Moving piece..."); // Switch players currentPlayer = (currentPlayer == 1) ? 2 : 1; } @Override protected boolean isGameOver() { if (movesCount >= MAX_MOVES) { System.out.println("Checkmate! Game Over!"); return true; } return false; } @Override protected void displayStatus() { System.out.println("Chess Status: Move " + movesCount + ", Player " + currentPlayer + "'s turn"); } @Override protected void displayWinner() { int winner = (currentPlayer == 1) ? 2 : 1; System.out.println("Winner is Player " + winner + "!"); } @Override protected boolean shouldShowStatus() { // Show status every 2 moves return movesCount % 2 == 0; }}// Concrete Class 2 - Checkers gamepublic class Checkers extends Game { private int currentPlayer = 1; private int piecesPlayer1 = 12; private int piecesPlayer2 = 12; private int turnsPlayed = 0; @Override protected void initializeGame() { System.out.println("Checkers Game Initialized!"); System.out.println("Setting up checkers board..."); System.out.println("Each player has 12 pieces"); currentPlayer = 1; piecesPlayer1 = 12; piecesPlayer2 = 12; turnsPlayed = 0; } @Override protected void makePlay() { turnsPlayed++; System.out.println("Player " + currentPlayer + " makes move..."); // Simulate capturing a piece if (turnsPlayed % 3 == 0) { if (currentPlayer == 1) { piecesPlayer2--; System.out.println("Player 1 captured a piece!"); } else { piecesPlayer1--; System.out.println("Player 2 captured a piece!"); } } // Switch players currentPlayer = (currentPlayer == 1) ? 2 : 1; } @Override protected boolean isGameOver() { if (piecesPlayer1 == 0) { System.out.println("All Player 1 pieces captured!"); return true; } if (piecesPlayer2 == 0) { System.out.println("All Player 2 pieces captured!"); return true; } if (turnsPlayed >= 20) { // Limit for demo System.out.println("Move limit reached!"); return true; } return false; } @Override protected void displayStatus() { System.out.println("Checkers Status:"); System.out.println(" Player 1 pieces: " + piecesPlayer1); System.out.println(" Player 2 pieces: " + piecesPlayer2); System.out.println(" Turns played: " + turnsPlayed); } @Override protected void displayWinner() { if (piecesPlayer1 > piecesPlayer2) { System.out.println("Player 1 wins with " + piecesPlayer1 + " pieces remaining!"); } else if (piecesPlayer2 > piecesPlayer1) { System.out.println("Player 2 wins with " + piecesPlayer2 + " pieces remaining!"); } else { System.out.println("It's a tie!"); } }}// Client codepublic class GameDemo { public static void main(String[] args) { System.out.println("=== PLAYING CHESS ===\n"); Game chess = new Chess(); chess.play(); System.out.println("\n" + "=".repeat(50) + "\n"); System.out.println("=== PLAYING CHECKERS ===\n"); Game checkers = new Checkers(); checkers.play(); }}🌍 Real-World Examples
- 🍳Recipe Following: Same steps (prep, cook, serve) but different ingredients
- 🏗️Build Processes: compile → test → package → deploy (same steps, different tools)
- 📊Data Import: read → validate → transform → save (same flow, different formats)
- 🌐Web Frameworks: request handling flow with customizable hooks
- 🧪Testing Frameworks: setUp → test → tearDown pattern
✅ Benefits
- ✅Code Reuse: Common code is written once in the base class
- ✅Control: The algorithm structure is controlled by the parent class
- ✅Flexibility: Subclasses can customize specific steps without affecting others
- ✅Consistency: Ensures all implementations follow the same algorithm flow
- ✅Maintenance: Changes to common steps only need to be made in one place
⚠️ Drawbacks
- ⚠️Inflexibility: The algorithm structure is fixed and cannot be changed by subclasses
- ⚠️Liskov Violation: Some implementations might become awkward if not all steps apply
- ⚠️Complexity: Increases number of classes and inheritance depth
- ⚠️Maintenance: Changes to the template method affect all subclasses
🔑 Key Points to Remember
- 1️⃣Template Method defines algorithm SKELETON, subclasses fill in the DETAILS
- 2️⃣The template method should be FINAL to prevent subclasses from changing the flow
- 3️⃣Use ABSTRACT methods for required steps, HOOKS for optional customization
- 4️⃣Hollywood Principle: "Don't call us, we'll call you" - parent calls child methods
- 5️⃣Common in frameworks where flow is fixed but behavior varies
- 6️⃣Different from Strategy - Strategy changes entire algorithm, Template Method changes steps
💪 Practice Scenarios
- • Create a document generator template for PDF, Word, and HTML with same structure
- • Build a data migration template for different database types
- • Implement an order processing template with different payment and shipping methods
- • Design a report generation template with different formats and data sources
- • Create an authentication template supporting different auth providers