Overview
The Template Method Pattern establishes an algorithm's invariant structure in a superclass while delegating certain steps to subclasses. This pattern defines a method that serves as a template, containing a sequence of method calls where some methods are abstract or have default implementations that subclasses can override.
The pattern originated in object-oriented design to address code duplication when multiple classes follow similar algorithmic structures but differ in specific implementation details. Instead of replicating the entire algorithm across classes, the Template Method Pattern extracts the common structure into a base class and isolates the varying parts into methods that subclasses override.
In Ruby, this pattern aligns naturally with the language's class-based inheritance model. The template method resides in a parent class and calls other methods (hook methods or primitive operations) that child classes implement or override. Ruby's dynamic nature and flexible method resolution make this pattern particularly expressive.
The pattern addresses several key scenarios: when multiple classes share algorithmic structure but differ in details, when code duplication across similar classes creates maintenance burden, or when an algorithm's invariant parts should be centralized while variant parts remain customizable.
# Basic structure
class DataProcessor
def process
load_data
validate_data
transform_data
save_data
end
def load_data
raise NotImplementedError
end
def validate_data
# Default implementation
true
end
def transform_data
raise NotImplementedError
end
def save_data
raise NotImplementedError
end
end
Key Principles
The Template Method Pattern operates on several fundamental principles that define its behavior and structure.
Inversion of Control forms the pattern's core mechanism. The parent class controls the algorithm's flow, calling methods that subclasses implement. This inverts the typical control flow where child classes would call parent methods. The parent class dictates when and in what order operations occur, while subclasses provide the concrete implementations.
Hook methods represent optional extension points that subclasses may override but are not required to. These methods typically provide default behavior or do nothing. Hook methods differ from abstract methods in that they allow, but do not mandate, customization. A hook method might log information, perform optional validation, or provide default transformations that subclasses can replace.
Primitive operations are abstract methods that subclasses must implement. These represent the variable parts of the algorithm that differ across implementations. The template method calls primitive operations at specific points in the algorithm, relying on subclasses to provide concrete behavior.
Concrete operations provide default implementations that subclasses inherit but may override. These operations represent common behavior shared across most or all subclasses. They reduce code duplication by centralizing shared logic while remaining overridable for exceptional cases.
The relationship between these components creates a contract: the parent class defines the algorithm's structure and guarantees its execution order, while subclasses fulfill the contract by implementing required operations and optionally customizing hook methods.
Method visibility plays an important role in the pattern. Template methods are typically public, as they represent the primary interface clients use. Primitive operations and hook methods often have protected or private visibility to signal they are internal implementation details not meant for direct client invocation.
class ReportGenerator
def generate
prepare_data
format_header
format_body
format_footer if include_footer?
finalize
end
protected
# Primitive operation - must implement
def prepare_data
raise NotImplementedError
end
# Primitive operation - must implement
def format_body
raise NotImplementedError
end
# Concrete operation - default implementation
def format_header
"Report generated at #{Time.now}\n\n"
end
# Hook method - optional override
def format_footer
"\n\nEnd of report"
end
# Hook method - optional override
def include_footer?
true
end
# Concrete operation
def finalize
# Common finalization logic
end
end
Liskov Substitution Principle applies directly to this pattern. Any subclass implementing the template method should be substitutable for the parent class without breaking the algorithm's correctness. The parent class establishes preconditions and postconditions that subclasses must respect.
Open/Closed Principle manifests through the pattern's design. The algorithm structure remains closed for modification (defined in the parent class) but open for extension (through subclass implementations of primitive operations and hook methods).
The pattern distinguishes between the algorithm's abstract structure and its concrete implementations. The parent class encodes knowledge about when operations occur and in what sequence, while subclasses encode knowledge about what those operations do. This separation of concerns improves maintainability and testability.
Ruby Implementation
Ruby's class inheritance and method resolution provide natural support for the Template Method Pattern. The language's dynamic features and conventions make the pattern particularly ergonomic.
Basic implementation uses class inheritance with methods that subclasses override:
class DocumentConverter
def convert(input_path, output_path)
content = read_file(input_path)
parsed = parse_content(content)
transformed = transform(parsed)
formatted = format_output(transformed)
write_file(output_path, formatted)
end
protected
def read_file(path)
File.read(path)
end
def parse_content(content)
raise NotImplementedError, "Subclasses must implement parse_content"
end
def transform(parsed_data)
parsed_data
end
def format_output(data)
raise NotImplementedError, "Subclasses must implement format_output"
end
def write_file(path, content)
File.write(path, content)
end
end
class MarkdownToHtmlConverter < DocumentConverter
protected
def parse_content(content)
# Parse markdown
content.split("\n").map { |line| parse_line(line) }
end
def transform(parsed_data)
# Additional transformations
parsed_data.map { |line| line.gsub(/\*\*(.+?)\*\*/, '<strong>\1</strong>') }
end
def format_output(data)
"<html><body>#{data.join("\n")}</body></html>"
end
private
def parse_line(line)
return "<h1>#{line[2..-1]}</h1>" if line.start_with?("# ")
return "<h2>#{line[3..-1]}</h2>" if line.start_with?("## ")
"<p>#{line}</p>"
end
end
converter = MarkdownToHtmlConverter.new
converter.convert("input.md", "output.html")
Handling abstract methods in Ruby differs from statically typed languages. Ruby has no built-in abstract method mechanism, so raising NotImplementedError serves as the convention:
class AbstractProcessor
def process(data)
validated = validate(data)
return nil unless validated
execute(validated)
end
protected
def validate(data)
# Default validation
!data.nil? && !data.empty?
end
def execute(data)
raise NotImplementedError, "#{self.class} must implement execute method"
end
end
Hook methods with default behavior provide extension points without mandating overrides:
class BatchProcessor
def process_batch(items)
before_batch(items)
results = items.map do |item|
before_item(item)
result = process_item(item)
after_item(item, result)
result
end
after_batch(results)
results
end
protected
def before_batch(items)
# Hook - default does nothing
end
def before_item(item)
# Hook - default does nothing
end
def process_item(item)
raise NotImplementedError
end
def after_item(item, result)
# Hook - default does nothing
end
def after_batch(results)
# Hook - default does nothing
end
end
class LoggingBatchProcessor < BatchProcessor
protected
def before_batch(items)
puts "Processing #{items.size} items"
end
def process_item(item)
item.upcase
end
def after_batch(results)
puts "Completed processing #{results.size} items"
end
end
Method visibility control uses Ruby's access modifiers to signal intent:
class Pipeline
# Public interface - template method
def execute(input)
setup
result = run_pipeline(input)
cleanup
result
end
protected
# Protected - internal template structure
def run_pipeline(input)
stage1 = process_stage_one(input)
stage2 = process_stage_two(stage1)
process_stage_three(stage2)
end
# Protected - primitive operations
def process_stage_one(input)
raise NotImplementedError
end
def process_stage_two(input)
raise NotImplementedError
end
def process_stage_three(input)
raise NotImplementedError
end
private
# Private - internal helpers not meant for override
def setup
# Setup logic
end
def cleanup
# Cleanup logic
end
end
Multiple template methods can coexist in the same class hierarchy:
class DataService
def fetch_and_process(query)
connection = connect
raw_data = fetch(connection, query)
process(raw_data)
ensure
disconnect(connection) if connection
end
def save_and_notify(data)
validate_data(data)
save(data)
notify_subscribers(data) if should_notify?
end
protected
def connect
raise NotImplementedError
end
def fetch(connection, query)
raise NotImplementedError
end
def process(data)
data
end
def disconnect(connection)
raise NotImplementedError
end
def validate_data(data)
raise ArgumentError, "Invalid data" if data.nil?
end
def save(data)
raise NotImplementedError
end
def notify_subscribers(data)
# Default notification
end
def should_notify?
true
end
end
Super calls allow subclasses to extend rather than replace parent behavior:
class ImageProcessor
def process(image)
before_process
result = apply_filters(image)
after_process
result
end
protected
def before_process
puts "Starting image processing"
end
def apply_filters(image)
raise NotImplementedError
end
def after_process
puts "Finished image processing"
end
end
class ThumbnailGenerator < ImageProcessor
protected
def before_process
super
puts "Generating thumbnail"
end
def apply_filters(image)
resize(image, 150, 150)
end
private
def resize(image, width, height)
# Resize implementation
{ data: image[:data], width: width, height: height }
end
end
Practical Examples
The Template Method Pattern applies across various domains where algorithmic structure remains constant while implementation details vary.
Web scraper framework demonstrates the pattern with different parsing strategies:
class WebScraper
def scrape(url)
html = fetch_page(url)
return nil unless html
parsed = parse_html(html)
extracted = extract_data(parsed)
cleaned = clean_data(extracted)
validate_and_return(cleaned)
end
protected
def fetch_page(url)
require 'net/http'
uri = URI(url)
response = Net::HTTP.get_response(uri)
response.body if response.is_a?(Net::HTTPSuccess)
end
def parse_html(html)
raise NotImplementedError
end
def extract_data(parsed)
raise NotImplementedError
end
def clean_data(data)
data.reject(&:empty?)
end
def validate_and_return(data)
data.empty? ? nil : data
end
end
class ProductScraper < WebScraper
protected
def parse_html(html)
# Simplified parsing
html.scan(/<div class="product">(.+?)<\/div>/m)
end
def extract_data(parsed)
parsed.map do |product_html|
{
name: extract_product_name(product_html[0]),
price: extract_price(product_html[0])
}
end
end
def clean_data(data)
super.select { |item| item[:price] && item[:price] > 0 }
end
private
def extract_product_name(html)
html[/<h3>(.+?)<\/h3>/, 1]
end
def extract_price(html)
price_str = html[/\$([0-9.]+)/, 1]
price_str ? price_str.to_f : nil
end
end
class ArticleScraper < WebScraper
protected
def parse_html(html)
html.scan(/<article>(.+?)<\/article>/m)
end
def extract_data(parsed)
parsed.map do |article_html|
{
title: article_html[0][/<h2>(.+?)<\/h2>/, 1],
author: article_html[0][/<span class="author">(.+?)<\/span>/, 1],
date: article_html[0][/<time>(.+?)<\/time>/, 1]
}
end
end
end
Game AI decision-making shows the pattern with different strategy implementations:
class AIController
def make_decision(game_state)
analyze_situation(game_state)
options = generate_options(game_state)
scored_options = score_options(options, game_state)
best_option = select_best(scored_options)
execute_action(best_option, game_state)
end
protected
def analyze_situation(game_state)
# Hook method - optional analysis
end
def generate_options(game_state)
raise NotImplementedError
end
def score_options(options, game_state)
raise NotImplementedError
end
def select_best(scored_options)
scored_options.max_by { |option| option[:score] }
end
def execute_action(option, game_state)
option[:action].call(game_state)
end
end
class AggressiveAI < AIController
protected
def generate_options(game_state)
[
{ action: ->(_) { :attack }, type: :attack },
{ action: ->(_) { :defend }, type: :defend }
]
end
def score_options(options, game_state)
options.map do |option|
score = case option[:type]
when :attack then calculate_attack_score(game_state)
when :defend then calculate_defense_score(game_state)
end
option.merge(score: score)
end
end
private
def calculate_attack_score(game_state)
game_state[:player_health] > 50 ? 100 : 60
end
def calculate_defense_score(game_state)
game_state[:player_health] > 50 ? 30 : 70
end
end
class DefensiveAI < AIController
protected
def analyze_situation(game_state)
@threat_level = game_state[:enemy_nearby] ? :high : :low
end
def generate_options(game_state)
[
{ action: ->(_) { :attack }, type: :attack },
{ action: ->(_) { :defend }, type: :defend },
{ action: ->(_) { :retreat }, type: :retreat }
]
end
def score_options(options, game_state)
options.map do |option|
base_score = base_scores[option[:type]]
modifier = @threat_level == :high ? threat_modifiers[option[:type]] : 0
option.merge(score: base_score + modifier)
end
end
private
def base_scores
{ attack: 40, defend: 70, retreat: 50 }
end
def threat_modifiers
{ attack: -20, defend: 30, retreat: 20 }
end
end
Test execution framework illustrates the pattern with different test types:
class TestRunner
def run_test(test_case)
setup_test(test_case)
begin
prepare_environment
result = execute_test(test_case)
verify_result(result, test_case)
rescue StandardError => e
handle_failure(e, test_case)
ensure
cleanup_test(test_case)
end
end
protected
def setup_test(test_case)
# Hook method
end
def prepare_environment
# Common preparation
@start_time = Time.now
end
def execute_test(test_case)
raise NotImplementedError
end
def verify_result(result, test_case)
raise NotImplementedError
end
def handle_failure(error, test_case)
{
status: :failed,
error: error.message,
test: test_case
}
end
def cleanup_test(test_case)
@end_time = Time.now
log_execution_time
end
private
def log_execution_time
duration = @end_time - @start_time
puts "Test completed in #{duration}s"
end
end
class UnitTestRunner < TestRunner
protected
def setup_test(test_case)
@mocks = []
@stubs = []
end
def execute_test(test_case)
test_case[:method].call
end
def verify_result(result, test_case)
expected = test_case[:expected]
{
status: result == expected ? :passed : :failed,
actual: result,
expected: expected
}
end
def cleanup_test(test_case)
@mocks.each(&:reset)
@stubs.each(&:reset)
super
end
end
class IntegrationTestRunner < TestRunner
protected
def setup_test(test_case)
setup_database
seed_test_data(test_case)
end
def execute_test(test_case)
test_case[:scenario].call
end
def verify_result(result, test_case)
checks = test_case[:assertions]
failures = checks.reject { |check| check.call(result) }
{
status: failures.empty? ? :passed : :failed,
failures: failures
}
end
def cleanup_test(test_case)
rollback_database
super
end
private
def setup_database
# Database setup
end
def seed_test_data(test_case)
# Insert test data
end
def rollback_database
# Clean database
end
end
Design Considerations
Selecting the Template Method Pattern requires evaluating several design factors and understanding when the pattern provides value versus when it introduces unnecessary complexity.
When to apply the pattern depends on specific code characteristics. Multiple classes implementing similar algorithms with shared structure but different details indicate a good candidate. Code duplication across classes where the duplication involves algorithmic flow rather than just individual methods suggests the pattern would centralize the common structure. Requirements for enforcing a specific sequence of operations across multiple implementations make the pattern valuable for ensuring consistency.
Inheritance depth affects the pattern's maintainability. Single-level inheritance (one abstract parent, multiple concrete children) remains straightforward and readable. Multi-level hierarchies where template methods call other template methods increase complexity and make the control flow harder to trace. Each additional inheritance level compounds the difficulty of understanding the complete algorithm execution path.
Flexibility trade-offs emerge between structure and customization. The pattern enforces a rigid algorithm structure, which provides consistency but limits flexibility. Subclasses cannot reorder operations or skip steps without violating the parent class's contract. When requirements demand variable operation ordering or conditional step execution, the pattern may be too restrictive.
Composition as alternative offers different trade-offs. The Strategy Pattern replaces inheritance with composition, allowing runtime algorithm selection and avoiding inheritance-related coupling. Composition provides more flexibility for combining behaviors but requires more upfront design and potentially more objects. The Template Method Pattern works better when the algorithm structure is truly invariant and inheritance relationships align with the domain model.
# Template Method - rigid structure
class ReportBuilder
def build
fetch_data
format_data
generate_output
end
end
# Strategy - flexible composition
class ReportBuilder
def initialize(data_fetcher, formatter, generator)
@data_fetcher = data_fetcher
@formatter = formatter
@generator = generator
end
def build
data = @data_fetcher.fetch
formatted = @formatter.format(data)
@generator.generate(formatted)
end
end
Testability considerations differ between approaches. Template Method Pattern requires testing through inheritance, meaning tests instantiate concrete subclasses to verify behavior. This can make unit testing primitive operations in isolation more challenging. Composition-based alternatives allow testing each component independently by injecting test doubles.
Method granularity affects the pattern's usability. Too many small primitive operations fragment the algorithm and force subclasses to implement numerous methods. Too few large operations reduce flexibility and may require subclasses to duplicate logic. Finding the right balance requires understanding which variations actually occur in practice versus which are theoretical possibilities.
Domain alignment influences whether inheritance represents a natural relationship. The Template Method Pattern works best when the inheritance hierarchy reflects genuine "is-a" relationships in the domain. Forcing unrelated classes into an inheritance hierarchy solely to share algorithmic structure creates artificial coupling and violates domain semantics.
Hook method proliferation can clutter the interface. Each hook method adds a potential extension point, but too many hooks make the class harder to understand and use. Subclasses face decision fatigue determining which hooks to override. Including only hooks that address actual variation points rather than hypothetical future needs maintains clarity.
Change frequency impacts maintenance burden. When the algorithm structure changes frequently, modifying the parent class affects all subclasses. When individual implementations change frequently but the overall structure remains stable, the pattern isolates changes effectively. Assessing which aspect changes more helps determine if the pattern's benefits outweigh its constraints.
Common Patterns
Several established patterns and variations emerge when implementing Template Methods in practice.
Abstract class with NotImplementedError represents the standard Ruby approach for enforcing subclass implementation:
class AbstractService
def perform
prepare
result = execute
finalize(result)
result
end
protected
def prepare
# Default preparation
end
def execute
raise NotImplementedError, "#{self.class} must implement #execute"
end
def finalize(result)
# Default finalization
end
end
class ConcreteService < AbstractService
protected
def execute
# Actual implementation
{ status: :success }
end
end
Factory method integration combines template methods with object creation:
class DocumentProcessor
def process
parser = create_parser
validator = create_validator
parsed = parser.parse(input)
return nil unless validator.valid?(parsed)
transform(parsed)
end
protected
def create_parser
raise NotImplementedError
end
def create_validator
raise NotImplementedError
end
def transform(data)
raise NotImplementedError
end
end
class JsonDocumentProcessor < DocumentProcessor
protected
def create_parser
JsonParser.new
end
def create_validator
JsonValidator.new(schema)
end
def transform(data)
# JSON-specific transformation
end
private
def schema
# Return JSON schema
end
end
Multi-phase template breaks complex algorithms into distinct phases:
class DataPipeline
def run(input)
# Phase 1: Acquisition
raw_data = acquire_data(input)
return nil unless raw_data
# Phase 2: Transformation
transformed = transform_phase(raw_data)
return nil unless transformed
# Phase 3: Output
output_phase(transformed)
end
protected
def acquire_data(input)
load(input)
end
def transform_phase(data)
validated = validate(data)
return nil unless validated
cleaned = clean(validated)
enrich(cleaned)
end
def output_phase(data)
formatted = format(data)
deliver(formatted)
end
# Primitive operations
def load(input)
raise NotImplementedError
end
def validate(data)
true
end
def clean(data)
data
end
def enrich(data)
raise NotImplementedError
end
def format(data)
raise NotImplementedError
end
def deliver(formatted)
raise NotImplementedError
end
end
Conditional hook execution uses predicate methods to control hook invocation:
class CachedOperation
def execute(key)
cached = fetch_from_cache(key) if use_cache?
return cached if cached
result = perform_operation(key)
store_in_cache(key, result) if use_cache? && cache_result?(result)
result
end
protected
def perform_operation(key)
raise NotImplementedError
end
def use_cache?
true
end
def cache_result?(result)
true
end
def fetch_from_cache(key)
# Cache fetch implementation
end
def store_in_cache(key, result)
# Cache store implementation
end
end
class ExpensiveOperation < CachedOperation
protected
def perform_operation(key)
# Expensive computation
sleep(2)
"result for #{key}"
end
def cache_result?(result)
!result.nil? && !result.empty?
end
end
class RealTimeOperation < CachedOperation
protected
def perform_operation(key)
# Time-sensitive operation
Time.now.to_s
end
def use_cache?
false
end
end
Error handling template centralizes exception handling patterns:
class ResilientOperation
def execute
attempts = 0
max_attempts = retry_limit
begin
attempts += 1
before_attempt(attempts)
result = perform
after_successful_attempt(result)
result
rescue StandardError => e
if should_retry?(e, attempts, max_attempts)
wait_before_retry(attempts)
retry
else
handle_final_failure(e, attempts)
end
end
end
protected
def perform
raise NotImplementedError
end
def retry_limit
3
end
def should_retry?(error, attempts, max_attempts)
attempts < max_attempts && retryable_error?(error)
end
def retryable_error?(error)
true
end
def wait_before_retry(attempts)
sleep(attempts * 0.5)
end
def before_attempt(attempts)
# Hook
end
def after_successful_attempt(result)
# Hook
end
def handle_final_failure(error, attempts)
raise error
end
end
State-carrying template maintains state across hook invocations:
class StatefulProcessor
def process(items)
@state = initialize_state
items.each do |item|
process_item(item)
update_state(item)
end
finalize_state
end
protected
attr_reader :state
def initialize_state
{ processed: 0, errors: 0 }
end
def process_item(item)
raise NotImplementedError
end
def update_state(item)
@state[:processed] += 1
end
def finalize_state
@state
end
end
class ValidatingProcessor < StatefulProcessor
protected
def process_item(item)
if valid?(item)
transform(item)
else
handle_invalid(item)
end
end
def update_state(item)
super
@state[:errors] += 1 unless valid?(item)
end
private
def valid?(item)
!item.nil? && item.respond_to?(:to_s)
end
def transform(item)
item.to_s.upcase
end
def handle_invalid(item)
# Error handling
end
end
Common Pitfalls
Several mistakes commonly occur when implementing or using the Template Method Pattern.
Excessive abstraction happens when developers create template methods for algorithms that have insufficient variation across implementations. Creating an abstract base class with multiple primitive operations when only one or two subclasses exist or when the variations are minimal introduces unnecessary complexity without corresponding benefit. The pattern adds value when multiple concrete implementations share substantial algorithmic structure, not when variations are minor or implementations are few.
# Over-abstracted - only one subclass needed
class DataLoader
def load
connect
fetch
disconnect
end
protected
def connect; raise NotImplementedError; end
def fetch; raise NotImplementedError; end
def disconnect; raise NotImplementedError; end
end
# Better - direct implementation
class FileLoader
def load(path)
File.read(path)
end
end
Fragile base class problem arises when subclasses depend on parent class implementation details rather than just the contract. Changes to the parent class's internal methods or instance variables break subclasses that access or modify them. This coupling makes the hierarchy brittle and difficult to evolve.
# Fragile - subclass depends on parent internals
class BaseProcessor
def process
@data = load_data
transform_internal
end
protected
def transform_internal
@data.map(&:upcase)
end
end
class SubProcessor < BaseProcessor
protected
def transform_internal
@data.map(&:downcase) # Depends on @data existing
end
end
# Better - explicit parameter passing
class BaseProcessor
def process
data = load_data
transform(data)
end
protected
def transform(data)
data.map(&:upcase)
end
end
Incorrect hook method defaults occur when default implementations make assumptions that don't hold for all subclasses. A hook method should either do nothing by default or perform truly universal operations. Defaults that work for some but not all subclasses force subclasses to override with empty methods, which signals a design problem.
Deep inheritance hierarchies reduce code comprehension by spreading algorithm implementation across multiple classes. Tracing execution requires jumping between parent and child classes repeatedly. Each inheritance level adds cognitive load and makes debugging more difficult. Limiting template method hierarchies to one or two levels maintains understandability.
Violated substitutability happens when subclasses override methods in ways that break the parent class's expectations. If the template method assumes certain preconditions or postconditions for primitive operations, subclasses must respect those assumptions. Violations lead to subtle bugs that surface only in specific execution scenarios.
# Violated contract
class BaseValidator
def validate(data)
# Assumes check_required returns boolean
return false unless check_required(data)
check_format(data)
end
protected
def check_required(data)
raise NotImplementedError
end
def check_format(data)
true
end
end
class BrokenValidator < BaseValidator
protected
def check_required(data)
nil # Returns nil instead of boolean - breaks template logic
end
end
Missing NotImplementedError in Ruby makes the absence of required methods harder to detect. Without explicitly raising NotImplementedError, calling an unimplemented method results in NoMethodError at runtime, which provides less informative error messages and delays error detection.
Overriding template methods themselves rather than primitive operations breaks the pattern. Subclasses should override the steps (primitive operations) but not the template method that orchestrates them. Overriding the template method defeats the purpose of centralizing the algorithm structure.
State management confusion emerges when instance variables set in the template method are accessed or modified in primitive operations without clear documentation. Implicit state sharing between the template method and primitive operations creates hidden dependencies and makes behavior harder to predict.
# Confusing state management
class Processor
def process
@state = :initializing
step_one
@state = :processing
step_two
@state = :complete
end
protected
def step_one
# Unclear what @state should be here
end
def step_two
# Subclass doesn't know it can access @state
end
end
# Clearer - explicit parameters
class Processor
def process
context = { state: :initializing }
step_one(context)
context[:state] = :processing
step_two(context)
end
protected
def step_one(context)
# Context is explicit parameter
end
def step_two(context)
# Context is explicit parameter
end
end
Insufficient documentation leaves subclass implementers uncertain about requirements. Each primitive operation needs documentation specifying parameters, return values, side effects, and any assumptions the template method makes about the operation's behavior. Without this information, implementers may create correct-looking but semantically incorrect implementations.
Reference
Pattern Structure
| Component | Description | Characteristics |
|---|---|---|
| Abstract Class | Defines template method and primitive operations | Contains algorithm skeleton |
| Template Method | Public method orchestrating algorithm steps | Calls primitive operations in defined order |
| Primitive Operation | Abstract method requiring subclass implementation | Represents variable algorithm step |
| Hook Method | Optional extension point with default implementation | Allows but does not require override |
| Concrete Class | Implements primitive operations | Provides specific behavior |
Method Types
| Type | Implementation | Override Requirement | Visibility |
|---|---|---|---|
| Template Method | Concrete in parent | Should not override | Public |
| Primitive Operation | Abstract in parent | Must override | Protected |
| Hook Method | Default in parent | May override | Protected |
| Concrete Operation | Concrete in parent | May override | Protected/Private |
Implementation Checklist
| Step | Action | Verification |
|---|---|---|
| Identify invariant structure | Extract common algorithm flow across classes | Algorithm steps same across implementations |
| Define abstract class | Create parent class with template method | Template method orchestrates algorithm |
| Identify primitive operations | Determine varying steps needing implementation | Each variation point has primitive operation |
| Add hook methods | Create optional extension points | Default behavior appropriate for most cases |
| Set method visibility | Make template public, primitives protected | Interface clear and protected from misuse |
| Document contracts | Specify preconditions and postconditions | Subclass implementers understand requirements |
| Implement concrete classes | Override primitive operations and desired hooks | Each subclass provides complete implementation |
Design Decision Matrix
| Consideration | Template Method | Strategy Pattern | When to Prefer |
|---|---|---|---|
| Algorithm structure | Fixed in parent | Variable through composition | Template Method: Structure truly invariant |
| Flexibility | Limited to primitive operations | High runtime flexibility | Strategy: Need runtime algorithm swapping |
| Coupling | Parent-child inheritance coupling | Loose coupling via interfaces | Strategy: Avoid inheritance coupling |
| Number of variations | Best for 2-5 implementations | Scales to many strategies | Strategy: Many algorithm variants |
| Change frequency | Structure changes affect all subclasses | Easy to add new strategies | Strategy: Frequent algorithm additions |
Common Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Single subclass | Unnecessary abstraction | Implement directly without parent |
| Empty hook overrides | Default implementation not universal | Reconsider default or make primitive |
| Template override | Subclass replaces template method | Override only primitive operations |
| Deep inheritance | Algorithm spread across multiple levels | Flatten hierarchy, use composition |
| Implicit state | Hidden instance variable dependencies | Pass state explicitly as parameters |
| Missing contracts | Unclear primitive operation requirements | Document preconditions and postconditions |
Ruby Idioms
| Idiom | Implementation | Purpose |
|---|---|---|
| NotImplementedError | raise NotImplementedError in abstract method | Signal required implementation |
| Protected visibility | protected keyword for primitive operations | Internal API for subclasses |
| Super calls | super in overridden method | Extend parent behavior |
| Method presence check | respond_to? before calling optional method | Graceful handling of optional overrides |
| Documentation | YARD comments on primitive operations | Clarify contracts for implementers |