Overview
Behavioral patterns form one of three categories in the Gang of Four design pattern taxonomy, alongside creational and structural patterns. These patterns address how objects interact and distribute responsibilities, focusing on the communication and collaboration between objects rather than their composition or creation.
The behavioral pattern category emerged from the observation that object-oriented systems require more than just proper object structure. Objects must coordinate their actions, delegate responsibilities, and manage complex workflows. Behavioral patterns provide solutions for these interaction challenges by encapsulating algorithms, defining communication protocols, and establishing clear responsibility chains.
These patterns operate at different levels of granularity. Some, like Strategy and Command, encapsulate individual algorithms or requests. Others, like Observer and Mediator, define communication structures between multiple objects. Template Method and State operate on object behavior over time, while Iterator and Visitor separate algorithms from the data structures they operate on.
Ruby's dynamic nature and features like blocks, procs, and module mixins make it particularly suited for implementing behavioral patterns. Many patterns that require verbose implementations in static languages become concise and expressive in Ruby. First-class functions through blocks and procs eliminate the need for separate command objects in many cases, while modules provide mixin-based behavior composition.
# Behavioral patterns define object communication
# Strategy pattern encapsulates algorithms
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def process(amount)
@strategy.call(amount)
end
end
credit_card = ->(amount) { "Charging $#{amount} to card" }
processor = PaymentProcessor.new(credit_card)
processor.process(100)
# => "Charging $100 to card"
The distinction between behavioral and other pattern types centers on intent. Creational patterns abstract object instantiation, structural patterns compose objects into larger structures, and behavioral patterns assign responsibilities for communication and algorithm management. A pattern qualifies as behavioral if its primary purpose involves object interaction protocols or responsibility distribution.
Key Principles
Behavioral patterns share several foundational principles that distinguish them from other pattern categories. Understanding these principles helps in pattern selection and implementation.
Encapsulation of Variation: Behavioral patterns encapsulate aspects of behavior that vary. Strategy encapsulates algorithms, State encapsulates state-dependent behavior, and Command encapsulates requests. This encapsulation allows variation points to change independently of the contexts that use them. The principle follows the "encapsulate what varies" maxim from design pattern literature.
Inversion of Control: Many behavioral patterns invert the usual control flow. Template Method defines algorithm structure but delegates specific steps to subclasses. Observer inverts dependencies so subjects notify observers without knowing their concrete types. This inversion reduces coupling and increases flexibility by allowing the framework to call application code rather than the reverse.
Composition Over Inheritance: Behavioral patterns favor object composition and delegation over class inheritance. Strategy composes contexts with algorithm objects, Command composes invokers with command objects, and Chain of Responsibility chains handler objects. This composition provides greater flexibility than inheritance-based solutions because behaviors can change at runtime and combine in ways not anticipated at design time.
# Template Method uses inheritance
class ReportGenerator
def generate
gather_data
format_output
send_report
end
def gather_data
raise NotImplementedError
end
def format_output
raise NotImplementedError
end
end
# Strategy uses composition
class ReportGenerator
def initialize(data_gatherer, formatter)
@data_gatherer = data_gatherer
@formatter = formatter
end
def generate
data = @data_gatherer.gather
@formatter.format(data)
end
end
Single Responsibility for Interactions: Each behavioral pattern addresses one specific interaction concern. Observer handles one-to-many notifications, Mediator manages many-to-many communications, Chain of Responsibility processes requests sequentially. This focused responsibility makes patterns composable and easier to understand.
Protocol Definition: Behavioral patterns define protocols or interfaces for object communication. These protocols specify how objects interact without dictating their internal implementation. Observer defines the subject-observer protocol, Command defines the command execution protocol, and Visitor defines the element-visitor double-dispatch protocol.
Temporal Coupling Management: Several behavioral patterns manage temporal relationships between operations. Memento captures object state at specific points in time, Command queues operations for delayed execution, and State changes behavior based on temporal progression through states. This temporal management separates when operations execute from how they execute.
The relationship between these principles creates a design philosophy where objects collaborate through well-defined protocols, responsibilities remain focused and cohesive, and variation points receive proper encapsulation. Ruby's features align naturally with these principles, making pattern implementations more concise than in languages requiring explicit interface declarations or lacking first-class functions.
Common Patterns
The behavioral pattern catalog contains eleven distinct patterns, each addressing specific interaction and responsibility distribution scenarios. Understanding each pattern's intent and structure enables appropriate pattern selection.
Chain of Responsibility decouples request senders from receivers by giving multiple objects a chance to handle requests. The pattern chains handler objects and passes requests along the chain until a handler processes the request or the chain ends. Middleware stacks in web frameworks exemplify this pattern, where each middleware can process a request, modify it, or pass it to the next handler.
Command encapsulates requests as objects, parameterizing clients with different requests and supporting undoable operations. The pattern transforms method calls into standalone objects containing all information needed to execute the method later. Ruby blocks and procs often replace explicit command objects, but the Command pattern remains valuable when commands require complex lifecycle management or persistence.
# Command pattern for undo/redo
class TextEditor
def initialize
@text = ""
@history = []
end
def execute(command)
command.execute(@text)
@history << command
end
def undo
command = @history.pop
command.undo(@text)
end
end
class InsertCommand
def initialize(position, char)
@position = position
@char = char
end
def execute(text)
text.insert(@position, @char)
end
def undo(text)
text.slice!(@position)
end
end
Iterator provides sequential access to collection elements without exposing the collection's internal representation. Ruby's Enumerable module implements this pattern pervasively, with methods like each, map, and select defining iteration protocols. Custom collections include Enumerable and define each to gain iterator functionality.
Mediator defines an object that encapsulates how a set of objects interact, promoting loose coupling by preventing objects from referring to each other explicitly. The pattern centralizes complex communications and control logic between related objects. Form validation systems often use mediators to coordinate field validations without fields directly referencing each other.
Memento captures and externalizes an object's internal state without violating encapsulation, enabling state restoration. The pattern provides state snapshots that can restore objects to previous states. Version control systems and undo mechanisms implement memento-like structures.
# Memento for state snapshots
class DocumentMemento
attr_reader :content, :timestamp
def initialize(content)
@content = content.dup
@timestamp = Time.now
end
end
class Document
attr_accessor :content
def create_memento
DocumentMemento.new(@content)
end
def restore(memento)
@content = memento.content.dup
end
end
Observer defines a one-to-many dependency between objects so that when one object changes state, all dependents receive notification and update automatically. Ruby's Observable module implements this pattern, though many modern Ruby applications use pub-sub libraries or reactive programming frameworks instead.
State allows an object to alter its behavior when its internal state changes, appearing to change its class. The pattern encapsulates state-specific behavior in separate state objects. Connection management, protocol implementations, and workflow systems frequently use the State pattern.
# State pattern for connection lifecycle
class TCPConnection
def initialize
@state = ClosedState.new(self)
end
def open
@state.open
end
def close
@state.close
end
def change_state(state)
@state = state
end
end
class OpenState
def initialize(connection)
@connection = connection
end
def open
"Already open"
end
def close
@connection.change_state(ClosedState.new(@connection))
"Connection closed"
end
end
class ClosedState
def initialize(connection)
@connection = connection
end
def open
@connection.change_state(OpenState.new(@connection))
"Connection opened"
end
def close
"Already closed"
end
end
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The pattern lets algorithms vary independently from clients that use them. Sorting with different comparison strategies, compression with different algorithms, and validation with different rule sets demonstrate this pattern.
Template Method defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. Subclasses redefine certain algorithm steps without changing the algorithm's structure. Ruby's hook methods like inherited, included, and method_missing often implement template method-like patterns.
Visitor represents an operation to be performed on elements of an object structure, letting you define new operations without changing the element classes. The pattern uses double dispatch to route operations based on both the visitor type and element type. Abstract syntax tree processing and document rendering systems commonly use visitors.
These eleven patterns address different behavioral concerns but share the common goal of managing object interactions and responsibility distribution. Pattern selection depends on the specific communication structure, flexibility requirements, and whether the focus lies on algorithms, requests, state transitions, or element traversal.
Ruby Implementation
Ruby's language features enable concise behavioral pattern implementations that often require less boilerplate than implementations in static languages. Understanding how Ruby features map to pattern concepts helps create idiomatic implementations.
Blocks and Procs for Strategy and Command: Ruby's blocks and procs naturally implement Strategy and Command patterns without requiring separate class hierarchies. Any algorithm or request that fits in a callable object becomes a strategy or command.
# Strategy with blocks
class DataProcessor
def process(data, &strategy)
strategy.call(data)
end
end
processor = DataProcessor.new
processor.process([1, 2, 3]) { |data| data.map { |x| x * 2 } }
# => [2, 4, 6]
# Command with procs
class Invoker
def initialize
@commands = []
end
def add_command(&command)
@commands << command
end
def execute_all
@commands.each(&:call)
end
end
Enumerable for Iterator: Ruby's Enumerable module provides iterator functionality to any class that implements each. This mixin approach eliminates the need for separate iterator classes in most cases.
class CustomCollection
include Enumerable
def initialize(*items)
@items = items
end
def each(&block)
@items.each(&block)
end
end
collection = CustomCollection.new(1, 2, 3)
collection.map { |x| x * 2 }
# => [2, 4, 6]
Modules for Template Method: Ruby modules implement template methods through method composition and hook methods. The included and extended hooks enable framework code to execute when modules mix into classes.
module Reportable
def generate_report
prepare_data
format_header
format_body
format_footer
end
def format_header
"=== Report Header ==="
end
def format_footer
"=== End Report ==="
end
# Classes must implement these
def prepare_data
raise NotImplementedError
end
def format_body
raise NotImplementedError
end
end
class SalesReport
include Reportable
def prepare_data
@data = {sales: 1000, profit: 200}
end
def format_body
"Sales: $#{@data[:sales]}\nProfit: $#{@data[:profit]}"
end
end
Method Objects for Complex Commands: When commands require state or complex execution logic, Ruby classes as command objects provide clean encapsulation. The call method creates a consistent interface across command types.
class Transaction
def initialize
@operations = []
end
def add(operation)
@operations << operation
end
def execute
@operations.each(&:call)
end
def rollback
@operations.reverse.each(&:undo)
end
end
class CreateRecordOperation
def initialize(table, data)
@table = table
@data = data
@id = nil
end
def call
@id = @table.insert(@data)
end
def undo
@table.delete(@id) if @id
end
end
State with Class-Based State Objects: Ruby's duck typing allows state objects to share a common interface without formal inheritance. State transitions happen by assigning different state objects to an instance variable.
class OrderStateMachine
attr_reader :state
def initialize
@state = PendingState.new(self)
end
def confirm
@state.confirm
end
def ship
@state.ship
end
def cancel
@state.cancel
end
def transition_to(state_class)
@state = state_class.new(self)
end
end
class PendingState
def initialize(context)
@context = context
end
def confirm
@context.transition_to(ConfirmedState)
"Order confirmed"
end
def ship
"Cannot ship pending order"
end
def cancel
@context.transition_to(CancelledState)
"Order cancelled"
end
end
Observer with Ruby Observable: Ruby's standard library includes Observable module implementing the Observer pattern. Objects extend Observable and call changed and notify_observers to broadcast state changes.
require 'observer'
class StockTicker
include Observable
attr_reader :price
def initialize(symbol)
@symbol = symbol
@price = 0
end
def update_price(new_price)
@price = new_price
changed
notify_observers(@symbol, @price)
end
end
class PriceAlert
def update(symbol, price)
puts "#{symbol} now at $#{price}"
end
end
ticker = StockTicker.new("TECH")
alert = PriceAlert.new
ticker.add_observer(alert)
ticker.update_price(150.50)
# => TECH now at $150.50
Method Missing for Dynamic Proxies: Ruby's method_missing enables dynamic proxies that intercept method calls, implementing patterns like Chain of Responsibility or Command without explicit handler/command classes.
class MethodLogger
def initialize(target)
@target = target
end
def method_missing(method, *args, &block)
puts "Calling #{method} with #{args.inspect}"
result = @target.send(method, *args, &block)
puts "Result: #{result.inspect}"
result
end
def respond_to_missing?(method, include_private = false)
@target.respond_to?(method, include_private)
end
end
logged_array = MethodLogger.new([1, 2, 3])
logged_array.map { |x| x * 2 }
# => Calling map with []
# => Result: [2, 4, 6]
Ruby's dynamic features reduce ceremonial code required for behavioral patterns, but the underlying pattern concepts remain important. Understanding when Ruby idioms implement patterns helps recognize design structures in Ruby codebases and choose between explicit pattern implementations and Ruby's built-in features.
Practical Examples
Behavioral patterns solve real-world coordination and interaction problems. These examples demonstrate pattern application in common scenarios.
Multi-Step Form Validation with Chain of Responsibility: Web applications often require complex validation where multiple validators examine input sequentially. Chain of Responsibility allows adding and ordering validators without coupling the form to specific validation logic.
class ValidationChain
def initialize
@validators = []
end
def add(validator)
@validators << validator
self
end
def validate(data)
@validators.each do |validator|
result = validator.validate(data)
return result unless result.valid?
end
ValidationResult.new(true)
end
end
class ValidationResult
attr_reader :valid, :errors
def initialize(valid, errors = [])
@valid = valid
@errors = errors
end
def valid?
@valid
end
end
class EmailValidator
def validate(data)
if data[:email] =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
ValidationResult.new(true)
else
ValidationResult.new(false, ["Invalid email format"])
end
end
end
class PasswordValidator
def validate(data)
if data[:password].length >= 8
ValidationResult.new(true)
else
ValidationResult.new(false, ["Password too short"])
end
end
end
chain = ValidationChain.new
.add(EmailValidator.new)
.add(PasswordValidator.new)
result = chain.validate(email: "user@example.com", password: "secret123")
Plugin System with Strategy and Template Method: Applications requiring extensible behavior without core modification combine Strategy for algorithm variation and Template Method for consistent plugin lifecycle.
module Plugin
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def plugin_name(name = nil)
@plugin_name = name if name
@plugin_name
end
end
def initialize(config = {})
@config = config
setup
end
def execute(context)
before_execution(context)
result = perform(context)
after_execution(context, result)
result
end
def setup
# Subclasses override for initialization
end
def before_execution(context)
# Hook for pre-processing
end
def after_execution(context, result)
# Hook for post-processing
end
def perform(context)
raise NotImplementedError
end
end
class ImageProcessingPlugin
include Plugin
plugin_name "Image Processor"
def perform(context)
image = context[:image]
strategy = context[:strategy] || :resize
send(strategy, image)
end
def resize(image)
"Resized #{image}"
end
def compress(image)
"Compressed #{image}"
end
end
plugin = ImageProcessingPlugin.new(max_size: 1000)
plugin.execute(image: "photo.jpg", strategy: :compress)
# => "Compressed photo.jpg"
State Machine for Order Processing: E-commerce systems require complex order state management. The State pattern encapsulates state-specific behavior and transitions.
class Order
attr_reader :items, :total, :state
def initialize(items)
@items = items
@total = items.sum { |item| item[:price] * item[:quantity] }
@state = DraftState.new(self)
end
def submit
@state.submit
end
def pay
@state.pay
end
def ship
@state.ship
end
def complete
@state.complete
end
def cancel
@state.cancel
end
def transition_to(new_state_class)
@state = new_state_class.new(self)
end
end
class DraftState
def initialize(order)
@order = order
end
def submit
@order.transition_to(SubmittedState)
"Order submitted for processing"
end
def pay
"Cannot pay draft order. Submit first."
end
def ship
"Cannot ship draft order"
end
def complete
"Cannot complete draft order"
end
def cancel
@order.transition_to(CancelledState)
"Draft cancelled"
end
end
class SubmittedState
def initialize(order)
@order = order
end
def submit
"Order already submitted"
end
def pay
@order.transition_to(PaidState)
"Payment processed: $#{@order.total}"
end
def ship
"Cannot ship unpaid order"
end
def complete
"Cannot complete unpaid order"
end
def cancel
@order.transition_to(CancelledState)
"Order cancelled with refund"
end
end
class PaidState
def initialize(order)
@order = order
end
def submit
"Order already submitted"
end
def pay
"Order already paid"
end
def ship
@order.transition_to(ShippedState)
"Order shipped to customer"
end
def complete
"Cannot complete unshipped order"
end
def cancel
"Cannot cancel paid order. Contact support."
end
end
Observer for Real-Time Dashboard Updates: Dashboards monitoring system metrics require real-time updates when data changes. Observer pattern decouples metric sources from display components.
class MetricsAggregator
def initialize
@observers = []
@metrics = {}
end
def add_observer(observer)
@observers << observer
observer.update(@metrics)
end
def remove_observer(observer)
@observers.delete(observer)
end
def update_metric(name, value)
@metrics[name] = value
notify_observers
end
private
def notify_observers
@observers.each { |observer| observer.update(@metrics) }
end
end
class DashboardWidget
attr_reader :display_name
def initialize(name, filter)
@display_name = name
@filter = filter
@current_value = nil
end
def update(metrics)
new_value = @filter.call(metrics)
if new_value != @current_value
@current_value = new_value
render
end
end
def render
puts "#{@display_name}: #{@current_value}"
end
end
aggregator = MetricsAggregator.new
cpu_widget = DashboardWidget.new(
"CPU Usage",
->(m) { m[:cpu_percent] }
)
memory_widget = DashboardWidget.new(
"Memory Available",
->(m) { m[:memory_free_mb] }
)
aggregator.add_observer(cpu_widget)
aggregator.add_observer(memory_widget)
aggregator.update_metric(:cpu_percent, 45.2)
# => CPU Usage: 45.2
aggregator.update_metric(:memory_free_mb, 2048)
# => Memory Available: 2048
These examples demonstrate how behavioral patterns solve coordination problems while maintaining loose coupling and code flexibility. Pattern selection depends on the specific interaction structure and whether the problem involves algorithms, requests, state transitions, or notifications.
Design Considerations
Selecting appropriate behavioral patterns requires understanding their strengths, limitations, and when each pattern applies best. Design decisions involve trade-offs between flexibility, complexity, and performance.
Pattern Selection Criteria: Different behavioral patterns address different interaction structures. Observer works for one-to-many notifications where observers remain independent. Mediator handles many-to-many communications where objects must coordinate but shouldn't reference each other directly. Chain of Responsibility processes requests sequentially with early termination. Command encapsulates requests for queuing, logging, or undo. Strategy encapsulates algorithms when multiple implementations exist for the same operation.
The interaction cardinality guides pattern choice. One-to-many relationships suggest Observer. Many-to-many relationships with coordination requirements suggest Mediator. Sequential processing with conditional continuation suggests Chain of Responsibility. State-dependent behavior suggests State pattern. Algorithm variation suggests Strategy.
Flexibility vs Complexity Trade-offs: Behavioral patterns increase flexibility at the cost of additional classes and indirection. Strategy replaces conditional logic with strategy objects, trading simple conditionals for object composition and potential runtime strategy switching. State replaces state conditionals with state objects, enabling complex state transitions but requiring state class management. Chain of Responsibility replaces nested conditionals with handler chains, providing flexibility but obscuring the request path.
The flexibility benefits must justify the added complexity. Applications requiring frequent algorithm changes benefit from Strategy despite extra classes. Systems with complex state machines benefit from State pattern despite state class proliferation. Simple scenarios with stable algorithms rarely need these abstractions.
Performance Implications: Behavioral patterns introduce performance overhead through additional object creation, indirection, and method calls. Observer pattern notification loops scale linearly with observer count. Command pattern object creation adds allocation overhead for each command. Visitor pattern double dispatch requires two virtual method calls per element visit.
Performance costs remain negligible in most applications, but high-frequency operations or real-time systems require measurement. Caching strategy objects, pooling command objects, or using direct method calls instead of patterns may prove necessary in performance-critical paths.
Testing Considerations: Behavioral patterns generally improve testability by isolating variation points. Strategy objects test independently from contexts. Command objects test without executing commands. Observer pattern allows testing subjects and observers separately. Chain of Responsibility enables testing individual handlers without the complete chain.
The improved testability assumes proper dependency injection. Patterns using hard-coded dependencies (new SpecificStrategy inside a context) lose testing benefits. Dependency injection frameworks or manual constructor injection enable test double substitution.
Maintenance and Understandability: Behavioral patterns affect code maintainability in opposing ways. Patterns improve maintainability by encapsulating variation and providing consistent structures. Developers familiar with patterns recognize Observer implementations or Strategy hierarchies immediately. Patterns reduce code duplication by extracting common behavior.
Conversely, patterns add indirection that obscures program flow. Following execution through multiple pattern objects requires understanding pattern mechanics. Overuse creates architecture astronomy where finding concrete behavior requires traversing many abstractions. The maintenance benefit appears when variation points change frequently and pattern structure remains stable.
Ruby Idiom vs Explicit Pattern: Ruby often provides idiomatic alternatives to explicit pattern implementations. Blocks replace simple Strategy and Command objects. Modules replace Template Method in many cases. Enumerable replaces Iterator. The choice between Ruby idioms and explicit patterns depends on pattern complexity and abstraction requirements.
Use Ruby idioms when behavior remains simple and pattern structure provides no benefit. Use explicit patterns when behavior requires state management, lifecycle control, or formal abstraction boundaries. The pattern makes sense if removing it increases coupling or reduces clarity.
# Simple case - Ruby idiom
collection.sort_by { |item| item.priority }
# Complex case - Explicit strategy
class PriorityStrategy
def initialize(weights)
@weights = weights
end
def compare(a, b)
score_a = calculate_score(a)
score_b = calculate_score(b)
score_b <=> score_a
end
def calculate_score(item)
@weights[:urgency] * item.urgency +
@weights[:importance] * item.importance +
@weights[:effort] * (1.0 / item.effort)
end
end
Design decisions balance flexibility needs against complexity costs while considering Ruby's expressive capabilities. Patterns prove valuable when variation points change independently, multiple implementations coexist, or formal abstraction boundaries improve clarity.
Common Pitfalls
Behavioral pattern implementations encounter recurring problems that reduce their effectiveness or introduce bugs. Recognizing these pitfalls helps avoid them.
Observer Memory Leaks: Observer implementations that fail to remove observers create memory leaks. Observers hold references preventing garbage collection even when observers should become unreferenced.
# Memory leak - observer never removed
class Subject
def initialize
@observers = []
end
def add_observer(observer)
@observers << observer
end
def notify
@observers.each { |o| o.update(self) }
end
end
subject = Subject.new
1000.times do
observer = Observer.new
subject.add_observer(observer)
# Observer should be garbage collected but subject keeps reference
end
Solution: Implement remove_observer and ensure observers unregister when finished. Use weak references for automatic cleanup, though Ruby lacks built-in weak reference support in older versions.
Command Pattern Memory Explosion: Systems queuing commands without bounds accumulate commands indefinitely. Undo systems keeping all command history consume memory proportional to action count.
# Unbounded command history
class UndoManager
def initialize
@history = []
end
def execute(command)
command.execute
@history << command # Grows without limit
end
def undo
command = @history.pop
command.undo
end
end
Solution: Implement history limits, discard old commands, or use command compression. Track memory usage and prune history when exceeding thresholds.
Strategy Explosion with Combinatorial Strategies: Systems with multiple variation dimensions create combinatorial strategy explosions. A report system with format strategies and delivery strategies requires separate strategy classes for each combination if improperly designed.
# Strategy explosion
class PDFEmailReport < Report
# Combines PDF format and email delivery
end
class PDFPrintReport < Report
# Combines PDF format and print delivery
end
class HTMLEmailReport < Report
# Combines HTML format and email delivery
end
# Requires N * M classes for N formats and M deliveries
Solution: Use composition with multiple strategy dimensions rather than single strategy hierarchies. Separate format strategy from delivery strategy.
class Report
def initialize(format_strategy, delivery_strategy)
@format = format_strategy
@delivery = delivery_strategy
end
def generate
content = @format.format(data)
@delivery.deliver(content)
end
end
State Pattern with Shared State: State objects that share mutable state between state instances create race conditions and bugs. Each state object might modify shared data assuming exclusive access.
Solution: Store all mutable data in the context object, not in state objects. State objects should remain stateless except for the context reference. Multiple state instances should safely coexist.
Chain of Responsibility Without Termination: Handler chains that fail to terminate cause requests to fall through without processing. No error occurs but the request never gets handled.
class HandlerChain
def initialize
@handlers = []
end
def handle(request)
@handlers.each do |handler|
result = handler.handle(request)
return result if result # Terminates on first success
end
nil # Request unhandled - silent failure
end
end
Solution: Raise exceptions for unhandled requests or return explicit unhandled indicators. Implement default handlers as chain terminators.
Visitor with Unstable Object Structures: Visitor pattern becomes maintenance burden when element hierarchies change frequently. Each new element type requires updating all visitor classes with new visit methods.
Solution: Avoid Visitor for frequently changing structures. Consider alternative approaches like type-based dispatch tables or dynamic dispatch through method_missing. Use Visitor only for stable element hierarchies with many operations.
Template Method Fragility: Template methods with many hook methods create fragile base classes. Subclasses must implement numerous methods correctly, and changes to the template algorithm break subclasses.
Solution: Minimize required hook methods. Provide sensible defaults for optional hooks. Document the template algorithm and hook responsibilities clearly. Consider Strategy instead of Template Method when variation points outnumber commonalities.
Mediator Becoming God Object: Mediators that control too many components and handle too many interaction types become monolithic God Objects. The mediator concentrates all coordination logic, becoming a maintenance bottleneck.
# Mediator doing too much
class ApplicationMediator
def handle_user_action(action)
# Coordinates UI, business logic, persistence, notifications...
end
def handle_data_change(data)
# Updates views, caches, indexes, logs...
end
def handle_system_event(event)
# Manages timers, background jobs, webhooks...
end
end
Solution: Split mediators by subsystem or concern. Create separate mediators for UI coordination, business logic coordination, and infrastructure coordination. Each mediator should manage a cohesive set of interactions.
Command Interface Inconsistency: Command objects with inconsistent interfaces (some use execute, others run, others call) reduce pattern effectiveness. Commands should follow uniform interfaces for generic handling.
Solution: Standardize on single method names. Ruby convention favors call for command-like objects, matching proc and lambda behavior. Implement to_proc to enable command objects as blocks.
Avoiding these pitfalls requires attention to pattern responsibilities, resource management, and system boundaries. Patterns should simplify rather than complicate, and implementations should match actual complexity requirements rather than adding speculative flexibility.
Reference
Behavioral Pattern Catalog
| Pattern | Intent | Key Participants | Applicability |
|---|---|---|---|
| Chain of Responsibility | Decouple request sender from receiver by chaining handlers | Handler, Concrete Handlers, Client | Processing requests with multiple potential handlers, middleware stacks, event bubbling |
| Command | Encapsulate requests as objects | Command, Concrete Commands, Invoker, Receiver | Undo/redo, request queuing, transaction logging, macro recording |
| Iterator | Provide sequential access without exposing representation | Iterator, Concrete Iterator, Aggregate | Traversing collections, custom iteration logic, multiple traversal algorithms |
| Mediator | Encapsulate object interactions | Mediator, Concrete Mediator, Colleagues | Complex object interactions, reducing coupling between components, centralizing control logic |
| Memento | Capture and restore object state | Memento, Originator, Caretaker | Undo mechanisms, snapshots, checkpointing, transaction rollback |
| Observer | Define one-to-many dependency for state notifications | Subject, Concrete Subject, Observer, Concrete Observers | Event handling, publish-subscribe, model-view separation, reactive updates |
| State | Alter behavior based on internal state | State, Concrete States, Context | State-dependent behavior, state machines, protocol implementations |
| Strategy | Encapsulate interchangeable algorithms | Strategy, Concrete Strategies, Context | Algorithm variation, policy selection, configurable behavior |
| Template Method | Define algorithm skeleton, defer steps to subclasses | Abstract Class, Concrete Classes | Invariant algorithm structure with variable steps, framework hooks |
| Visitor | Define operations on elements without changing element classes | Visitor, Concrete Visitors, Element, Concrete Elements | Operations on stable hierarchies, double dispatch, AST processing |
Pattern Relationships
| Pattern Pair | Relationship | Notes |
|---|---|---|
| Strategy vs State | State transitions automatically, Strategy requires explicit switching | State manages temporal progression, Strategy encapsulates algorithm choice |
| Command vs Strategy | Command encapsulates requests, Strategy encapsulates algorithms | Commands often contain receiver and parameters, strategies contain computation |
| Observer vs Mediator | Observer distributes changes, Mediator centralizes communication | Observer for one-to-many, Mediator for many-to-many coordination |
| Chain of Responsibility vs Command | Chain processes request, Command encapsulates request | Can combine: chain of command handlers |
| Template Method vs Strategy | Template Method uses inheritance, Strategy uses composition | Template Method for stable structures, Strategy for runtime variation |
| Iterator vs Visitor | Iterator accesses elements, Visitor operates on elements | Often combined: iterator provides access, visitor provides operation |
Ruby Implementation Idioms
| Pattern | Ruby Idiom | When to Use Explicit Pattern |
|---|---|---|
| Strategy | Blocks and procs | Complex strategies with state, multiple strategy methods, strategy reuse across contexts |
| Command | Blocks and procs | Command lifecycle management, undo/redo, command serialization, complex parameter handling |
| Iterator | Enumerable module | Custom iteration state, external iterators, bidirectional iteration |
| Template Method | Module mixins with hooks | Multiple hook methods, complex lifecycle, framework extension points |
| Observer | Observable module or pub-sub gems | Complex notification logic, observer filtering, asynchronous notifications |
| State | Direct conditional logic for simple cases | Complex state transitions, many states, state-specific behavior |
| Chain of Responsibility | Array of callables with find or each | Handler lifecycle, dynamic chain modification, handler statistics |
Common Method Names
| Pattern | Standard Methods | Ruby Conventions |
|---|---|---|
| Strategy | execute, do_algorithm | call (matches Proc interface) |
| Command | execute, undo, redo | call, undo, redo |
| State | handle, enter, exit | State-specific action methods, enter_state, exit_state |
| Observer | update, notify | update (Observable convention) |
| Visitor | visit, accept | visit_element_type, accept |
| Chain of Responsibility | handle, handle_request | call, handle |
Pattern Selection Decision Tree
| Question | Yes Path | No Path |
|---|---|---|
| Does behavior depend on object state that changes frequently? | Consider State pattern | Continue to next question |
| Do you need to encapsulate multiple algorithms for the same operation? | Consider Strategy pattern | Continue to next question |
| Do you need to notify multiple objects about state changes? | Consider Observer pattern | Continue to next question |
| Must multiple objects coordinate without direct references? | Consider Mediator pattern | Continue to next question |
| Do you need to process requests with multiple potential handlers? | Consider Chain of Responsibility | Continue to next question |
| Must you capture and restore object state? | Consider Memento pattern | Continue to next question |
| Do you need to queue, log, or undo requests? | Consider Command pattern | Continue to next question |
| Will you define operations on stable element hierarchies? | Consider Visitor pattern | Consider simpler approaches |
Memory Management Considerations
| Pattern | Memory Risk | Mitigation Strategy |
|---|---|---|
| Observer | Unbounded observer lists, memory leaks | Implement remove_observer, use weak references, automatic cleanup on garbage collection |
| Command | Command history accumulation | Implement history limits, command compression, periodic pruning |
| Memento | State snapshot accumulation | Limit snapshot count, implement expiration, compress snapshots |
| Chain of Responsibility | Long chains with state | Keep handlers stateless, limit chain length, monitor memory usage |
| Mediator | Centralized component references | Use weak references for optional components, implement cleanup |
| State | State object proliferation with state data | Keep state objects stateless, share immutable state objects, store mutable data in context |