Overview
The Mediator Pattern defines an object that encapsulates how a set of objects interact. Instead of objects referring to each other directly, they communicate through a mediator. This promotes loose coupling by preventing objects from explicitly referencing one another, allowing their interaction to vary independently.
Gang of Four introduced this pattern in their seminal Design Patterns book as a solution to the problem of tight coupling in systems where many objects interact in complex ways. Without a mediator, objects must maintain references to many other objects, creating a web of dependencies that becomes difficult to modify and understand.
The pattern applies to scenarios where multiple objects communicate in defined but complex ways, and the resulting interdependencies are unstructured and difficult to understand. Air traffic control systems exemplify this pattern: rather than pilots communicating directly with each other to coordinate landings and takeoffs, all communication flows through the control tower, which maintains the coordination logic centrally.
# Without Mediator: Direct coupling
class Airplane
def initialize
@other_planes = []
end
def request_landing
@other_planes.each do |plane|
return false if plane.landing?
end
land
end
end
# With Mediator: Centralized coordination
class AirTrafficControl
def initialize
@planes = []
end
def request_landing(plane)
return false if @planes.any?(&:landing?)
plane.land
true
end
end
The Mediator Pattern introduces a single point of control that reduces the complexity of communication protocols between objects. Rather than each object knowing about N other objects, each object knows only about the mediator, reducing coupling from O(N²) to O(N).
Key Principles
The Mediator Pattern operates on several foundational principles that define its structure and behavior. The pattern consists of four primary components: the Mediator interface, concrete Mediator implementations, the Colleague interface, and concrete Colleague implementations.
Mediator Interface: Defines the communication interface that colleagues use to send and receive messages. This interface abstracts the coordination logic, allowing different mediator implementations to provide different coordination strategies.
Concrete Mediator: Implements the coordination logic and maintains references to all colleague objects. The concrete mediator coordinates colleague behavior by receiving notifications from colleagues and directing which colleagues should respond. This component encapsulates the interaction logic that would otherwise be distributed across multiple colleague objects.
Colleague Interface: Defines the interface for objects that communicate through the mediator. Colleagues maintain a reference to their mediator but remain unaware of other colleague implementations, promoting loose coupling.
Concrete Colleagues: Implement specific behaviors and communicate exclusively through the mediator. When a colleague's state changes or an event occurs, it notifies the mediator rather than interacting with other colleagues directly.
The communication flow follows a specific pattern: a colleague sends a message to the mediator, the mediator processes the message according to its coordination logic, and the mediator forwards appropriate messages to relevant colleagues. This indirection eliminates direct colleague-to-colleague communication.
# Mediator interface
class ChatMediator
def send_message(message, sender)
raise NotImplementedError
end
def add_user(user)
raise NotImplementedError
end
end
# Concrete Mediator
class ChatRoom < ChatMediator
def initialize
@users = []
end
def add_user(user)
@users << user
user.mediator = self
end
def send_message(message, sender)
@users.each do |user|
user.receive(message) unless user == sender
end
end
end
# Colleague interface
class User
attr_accessor :mediator
attr_reader :name
def initialize(name)
@name = name
end
def send(message)
@mediator.send_message(message, self)
end
def receive(message)
puts "#{@name} received: #{message}"
end
end
The pattern creates a hub-and-spoke topology where the mediator acts as the hub and colleagues as spokes. This centralization transforms many-to-many relationships into one-to-many relationships, substantially reducing system complexity. The mediator maintains the complete state and logic needed for coordination, while colleagues focus on their individual responsibilities.
Encapsulation of collective behavior represents another key principle. Rather than distributing coordination logic across multiple objects, the mediator consolidates this logic in a single location. This consolidation simplifies modifications to interaction patterns, as changes occur in one place rather than being scattered across multiple colleague classes.
The pattern also enforces the principle of indirect communication. Colleagues never invoke methods on other colleagues directly; all communication passes through the mediator. This indirection provides flexibility to modify interaction patterns without changing colleague implementations.
Ruby Implementation
Ruby's dynamic nature and first-class functions provide several idiomatic approaches to implementing the Mediator Pattern. The most straightforward implementation uses standard class hierarchies, but Ruby's blocks, procs, and modules enable more expressive variations.
Basic Class-Based Implementation
The traditional object-oriented approach defines explicit mediator and colleague classes:
class FormMediator
def initialize
@components = {}
end
def register(name, component)
@components[name] = component
component.mediator = self
end
def notify(sender, event)
case event
when :username_changed
validate_username(sender)
when :email_changed
validate_email(sender)
when :submit
submit_form if valid?
end
end
private
def validate_username(field)
if field.value.length < 3
@components[:error].display("Username too short")
@components[:submit_button].disable
else
@components[:error].clear
@components[:submit_button].enable
end
end
def validate_email(field)
unless field.value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
@components[:error].display("Invalid email")
@components[:submit_button].disable
end
end
def valid?
@components.values.all?(&:valid?)
end
def submit_form
puts "Form submitted with values:"
@components.each { |name, comp| puts "#{name}: #{comp.value}" }
end
end
class FormField
attr_accessor :mediator
attr_reader :value
def initialize(name)
@name = name
@value = ""
end
def change(new_value)
@value = new_value
@mediator&.notify(self, :"#{@name}_changed")
end
def valid?
!@value.empty?
end
end
class Button
attr_accessor :mediator
def initialize
@enabled = false
end
def click
@mediator&.notify(self, :submit) if @enabled
end
def enable
@enabled = true
end
def disable
@enabled = false
end
def valid?
true
end
def value
@enabled.to_s
end
end
Block-Based Coordination
Ruby's blocks enable defining coordination logic inline, reducing the need for separate mediator classes:
class EventMediator
def initialize(&coordination_block)
@listeners = Hash.new { |h, k| h[k] = [] }
@coordination = coordination_block
instance_eval(&coordination_block) if coordination_block
end
def on(event, &handler)
@listeners[event] << handler
end
def emit(event, data = nil)
@listeners[event].each { |handler| handler.call(data) }
end
end
# Usage
mediator = EventMediator.new do
on(:order_placed) do |order|
emit(:inventory_update, order.items)
emit(:notification_send, order.customer)
emit(:payment_process, order.total)
end
on(:inventory_update) do |items|
items.each { |item| puts "Updating inventory for #{item}" }
end
on(:notification_send) do |customer|
puts "Sending notification to #{customer}"
end
end
mediator.emit(:order_placed, OpenStruct.new(
items: ['Widget', 'Gadget'],
customer: 'customer@example.com',
total: 99.99
))
Module-Based Mediation
Ruby modules provide another approach, mixing mediation capabilities into classes:
module Mediates
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def mediates(*colleague_types)
colleague_types.each do |type|
define_method("add_#{type}") do |colleague|
colleagues_of_type(type) << colleague
colleague.mediator = self
end
define_method("#{type}s") do
colleagues_of_type(type)
end
end
end
end
def colleagues_of_type(type)
@colleagues ||= Hash.new { |h, k| h[k] = [] }
@colleagues[type]
end
def notify_all(message, exclude: nil)
@colleagues.values.flatten.each do |colleague|
colleague.receive(message) unless colleague == exclude
end
end
end
class GameMediator
include Mediates
mediates :player, :observer
def player_moved(player, position)
players.each do |p|
p.update_position(player, position) unless p == player
end
observers.each do |o|
o.log_move(player, position)
end
end
def game_over(winner)
notify_all("Game over! Winner: #{winner}")
end
end
class Player
attr_accessor :mediator
attr_reader :name
def initialize(name)
@name = name
@position = [0, 0]
end
def move(x, y)
@position = [x, y]
@mediator.player_moved(self, @position)
end
def update_position(player, position)
puts "#{@name} sees #{player.name} at #{position}"
end
def receive(message)
puts "#{@name}: #{message}"
end
end
Observable Pattern Integration
Ruby's Observable module from the standard library integrates naturally with mediator implementations:
require 'observer'
class SystemMediator
include Observable
def initialize
@components = {}
end
def register(name, component)
@components[name] = component
add_observer(component)
end
def process_event(event_type, data)
changed
notify_observers(event_type, data)
end
end
class SystemComponent
def initialize(name)
@name = name
end
def update(event_type, data)
return unless handles?(event_type)
process(data)
end
def handles?(event_type)
respond_to?("handle_#{event_type}")
end
def process(data)
event_type = data[:type]
send("handle_#{event_type}", data) if handles?(event_type)
end
end
class Logger < SystemComponent
def handle_error(data)
puts "[ERROR] #{data[:message]}"
end
def handle_info(data)
puts "[INFO] #{data[:message]}"
end
end
class Alerter < SystemComponent
def handle_error(data)
puts "ALERT: Critical error - #{data[:message]}"
end
end
Thread-Safe Mediator
For concurrent applications, implementing thread-safe mediation requires synchronization:
require 'thread'
class ConcurrentMediator
def initialize
@colleagues = []
@mutex = Mutex.new
@queue = Queue.new
start_dispatcher
end
def register(colleague)
@mutex.synchronize do
@colleagues << colleague
colleague.mediator = self
end
end
def send_async(message, sender)
@queue << { message: message, sender: sender }
end
def send_sync(message, sender)
@mutex.synchronize do
broadcast(message, sender)
end
end
private
def start_dispatcher
@dispatcher = Thread.new do
loop do
msg_data = @queue.pop
@mutex.synchronize do
broadcast(msg_data[:message], msg_data[:sender])
end
end
end
end
def broadcast(message, sender)
@colleagues.each do |colleague|
colleague.receive(message) unless colleague == sender
end
end
end
Practical Examples
GUI Component Coordination
Dialog boxes with interdependent components demonstrate practical mediator usage. Form validation, button enabling, and error display require coordination that the mediator centralizes:
class DialogMediator
def initialize
@fields = {}
@validation_rules = {}
end
def register_field(name, field, &validation)
@fields[name] = field
@validation_rules[name] = validation
field.mediator = self
end
def field_changed(field, value)
validate_all
update_dependencies(field, value)
end
def submit_requested
if all_valid?
data = collect_data
puts "Submitting: #{data}"
true
else
display_errors
false
end
end
private
def validate_all
@errors = {}
@fields.each do |name, field|
validation = @validation_rules[name]
error = validation&.call(field.value)
@errors[name] = error if error
end
end
def all_valid?
@errors.empty?
end
def update_dependencies(changed_field, value)
case changed_field
when @fields[:country]
update_state_options(value)
when @fields[:membership_type]
update_pricing(value)
end
end
def update_state_options(country)
states = country == 'US' ? ['CA', 'NY', 'TX'] : []
@fields[:state]&.set_options(states)
end
def update_pricing(membership)
price = membership == 'premium' ? 99 : 49
@fields[:price_display]&.set_text("$#{price}/month")
end
def collect_data
@fields.transform_values(&:value)
end
def display_errors
@errors.each do |field, error|
puts "#{field}: #{error}"
end
end
end
class TextField
attr_accessor :mediator
attr_reader :value
def initialize
@value = ""
end
def set_value(new_value)
@value = new_value
@mediator&.field_changed(self, @value)
end
end
Chat Room System
Multi-user chat rooms require coordinating message delivery, user presence, and permission management:
class ChatRoomMediator
def initialize(room_name)
@room_name = room_name
@users = {}
@message_history = []
@moderators = Set.new
end
def join(user)
@users[user.id] = user
user.mediator = self
send_history(user)
broadcast_system_message("#{user.name} joined the room", exclude: user)
end
def leave(user)
@users.delete(user.id)
broadcast_system_message("#{user.name} left the room")
end
def send_message(sender, content)
return kick(sender, "Profanity not allowed") if contains_profanity?(content)
message = {
sender: sender.name,
content: content,
timestamp: Time.now
}
@message_history << message
@users.values.each { |user| user.receive_message(message) }
end
def send_private_message(sender, recipient_id, content)
recipient = @users[recipient_id]
return unless recipient
message = {
sender: sender.name,
content: content,
timestamp: Time.now,
private: true
}
recipient.receive_message(message)
sender.receive_message(message.merge(sent: true))
end
def promote_to_moderator(user)
@moderators << user.id
send_system_message(user, "You are now a moderator")
end
def kick(target, reason)
return unless moderator?(target.id) == false
send_system_message(target, "Kicked: #{reason}")
leave(target)
end
private
def moderator?(user_id)
@moderators.include?(user_id)
end
def contains_profanity?(content)
content.match?(/badword1|badword2/)
end
def send_history(user)
@message_history.last(50).each { |msg| user.receive_message(msg) }
end
def broadcast_system_message(content, exclude: nil)
message = { system: true, content: content, timestamp: Time.now }
@users.values.each do |user|
user.receive_message(message) unless user == exclude
end
end
def send_system_message(user, content)
user.receive_message({ system: true, content: content, timestamp: Time.now })
end
end
class ChatUser
attr_accessor :mediator
attr_reader :id, :name
def initialize(id, name)
@id = id
@name = name
end
def send(content)
@mediator.send_message(self, content)
end
def send_private(recipient_id, content)
@mediator.send_private_message(self, recipient_id, content)
end
def receive_message(message)
if message[:system]
puts "[SYSTEM] #{message[:content]}"
elsif message[:private]
prefix = message[:sent] ? "To" : "From"
puts "[#{prefix} #{message[:sender]}] #{message[:content]}"
else
puts "[#{message[:sender]}] #{message[:content]}"
end
end
end
Stock Trading System
Financial trading systems coordinate multiple services—order matching, risk management, notification, and audit logging:
class TradingMediator
def initialize
@order_book = OrderBook.new
@risk_manager = RiskManager.new
@notifier = NotificationService.new
@auditor = AuditLogger.new
[@order_book, @risk_manager, @notifier, @auditor].each do |service|
service.mediator = self
end
end
def place_order(order)
@auditor.log(:order_received, order)
unless @risk_manager.approve?(order)
@notifier.send(order.trader, "Order rejected: Risk limit exceeded")
@auditor.log(:order_rejected, order, reason: :risk)
return false
end
execution = @order_book.match(order)
if execution[:status] == :filled
@auditor.log(:order_filled, order, execution_price: execution[:price])
@notifier.send(order.trader, "Order filled at #{execution[:price]}")
@risk_manager.update_exposure(order.trader, execution)
true
else
@auditor.log(:order_partial, order, filled: execution[:filled])
@notifier.send(order.trader, "Order partially filled")
false
end
end
def cancel_order(order_id, trader)
removed = @order_book.remove(order_id, trader)
if removed
@auditor.log(:order_cancelled, order_id: order_id, trader: trader)
@notifier.send(trader, "Order cancelled")
@risk_manager.release_exposure(trader, removed)
true
else
false
end
end
def market_data_update(symbol, price)
@risk_manager.update_positions(symbol, price)
@notifier.broadcast_price(symbol, price)
end
end
class TradingService
attr_accessor :mediator
end
class OrderBook < TradingService
def initialize
@orders = {}
end
def match(order)
# Simplified matching logic
{ status: :filled, price: order.limit_price, filled: order.quantity }
end
def remove(order_id, trader)
@orders.delete(order_id)
end
end
class RiskManager < TradingService
def initialize
@exposure = Hash.new(0)
@limits = Hash.new(100_000)
end
def approve?(order)
potential_exposure = @exposure[order.trader] + order.value
potential_exposure <= @limits[order.trader]
end
def update_exposure(trader, execution)
@exposure[trader] += execution[:price] * execution[:filled]
end
def release_exposure(trader, order)
@exposure[trader] -= order.value
end
def update_positions(symbol, price)
# Mark-to-market updates
end
end
Design Considerations
The Mediator Pattern suits specific scenarios but introduces its own complexities. Understanding when to apply the pattern and when to avoid it requires analyzing the nature of object interactions and system evolution patterns.
When to Use the Mediator Pattern
Apply the mediator when object interactions form complex, hard-to-understand webs of dependencies. Systems where each object references multiple other objects create quadratic coupling—each new object potentially requires updates to all existing objects. The mediator linearizes this coupling, making the system easier to understand and modify.
Use the pattern when interaction logic changes frequently. Centralizing coordination logic in the mediator enables modifying interaction patterns without touching individual colleague classes. This separation particularly benefits systems where business rules governing object interactions evolve independently from the objects themselves.
Consider the mediator for systems requiring dynamic interaction patterns. When objects need to coordinate differently based on runtime state or configuration, the mediator provides a central point for implementing conditional coordination logic. Dialog boxes demonstrate this: field interdependencies vary based on user selections, and the mediator adjusts coordination accordingly.
The pattern excels in scenarios requiring cross-cutting concerns in object interactions. Logging, validation, and auditing apply to multiple interaction types. Rather than duplicating this logic across colleague classes, the mediator implements it once and applies it uniformly.
Trade-offs
The mediator centralizes complexity but doesn't eliminate it. The mediator object itself can become complex, containing intricate coordination logic. This concentration creates a "god object" risk—a single class responsible for too many concerns. Monitoring mediator complexity remains critical; mediators exceeding several hundred lines suggest splitting into multiple mediators or reconsidering the design.
The pattern introduces a single point of failure. All colleague communication flows through the mediator, making it critical to system operation. Bugs in the mediator affect all interactions, and mediator unavailability prevents any colleague communication. This risk necessitates robust testing and error handling in mediator implementations.
Performance implications emerge in high-throughput systems. Every colleague interaction involves mediator overhead—method calls, coordination logic, and potential data transformation. For latency-sensitive applications, this indirection may prove too expensive. Direct colleague communication, while increasing coupling, eliminates mediator overhead.
The mediator creates implicit coupling between colleagues and coordination logic. While colleagues avoid direct references to each other, they depend on the mediator's coordination behavior. Changing mediator logic affects all colleagues, creating a different form of coupling. This coupling proves more manageable than colleague-to-colleague coupling but still requires careful management.
Alternatives
The Observer pattern provides an alternative for one-way, broadcast communication. When objects need to notify others of state changes without requiring complex bidirectional coordination, observers decouple publishers from subscribers without a central mediator. The observer pattern suits event-driven systems where multiple objects react to state changes.
The Command pattern works when the coordination involves primarily request queuing and execution rather than complex bidirectional communication. Commands encapsulate requests as objects, enabling queuing, logging, and undo functionality without a mediator.
Service layers in application architecture serve mediator-like roles at higher abstraction levels. Rather than coordinating object interactions, service layers coordinate subsystem interactions. This approach scales better for large systems but operates at coarser granularity than the mediator pattern.
Event buses provide infrastructure-level mediation. Rather than custom mediator objects, event buses offer publish-subscribe mechanisms that decouple components through events. This approach works well for loosely coupled, scalable systems but adds infrastructure complexity.
Decision Criteria
Choose the mediator when colleague-to-colleague coupling exceeds mediator-to-colleague coupling in complexity. If coordinating N objects directly requires O(N²) relationships but a mediator requires O(N) relationships with simpler logic, the mediator wins.
Favor direct communication when interactions remain simple and stable. Two objects exchanging data without complex coordination logic don't benefit from mediation. The pattern overhead exceeds its benefits for straightforward relationships.
Consider the rate of change for interaction logic versus object logic. When interaction patterns change more frequently than object implementations, the mediator provides value by isolating interaction logic. When objects change frequently but interactions remain stable, the mediator adds unnecessary indirection.
Evaluate testability requirements. Mediators enable testing coordination logic independently from colleague implementations through mocking. Systems requiring extensive interaction testing benefit from this separation. Simple systems with minimal coordination logic don't justify the additional testing complexity mediators introduce.
Common Patterns
Several variations and extensions of the Mediator Pattern address specific scenarios and integrate with other design patterns to create comprehensive solutions.
Hierarchical Mediators
Complex systems require multiple mediators organized hierarchically. Rather than one massive mediator coordinating everything, hierarchical mediators delegate responsibilities:
class ApplicationMediator
def initialize
@subsystem_mediators = {}
end
def register_subsystem(name, mediator)
@subsystem_mediators[name] = mediator
mediator.parent_mediator = self
end
def route_message(source_subsystem, target_subsystem, message)
target = @subsystem_mediators[target_subsystem]
target&.receive_from_parent(source_subsystem, message)
end
end
class SubsystemMediator
attr_accessor :parent_mediator
def initialize(name)
@name = name
@components = []
end
def notify(component, message)
if message[:scope] == :local
handle_locally(component, message)
else
@parent_mediator.route_message(@name, message[:target], message)
end
end
def receive_from_parent(source, message)
@components.each { |c| c.receive_external(source, message) }
end
private
def handle_locally(component, message)
@components.each do |c|
c.receive_internal(message) unless c == component
end
end
end
Event-Driven Mediators
Event-driven architectures integrate naturally with mediators. Rather than direct method calls, colleagues publish events and the mediator subscribes and coordinates:
class EventDrivenMediator
def initialize
@subscribers = Hash.new { |h, k| h[k] = [] }
@event_queue = []
end
def subscribe(event_type, handler)
@subscribers[event_type] << handler
end
def publish(event)
@event_queue << event
process_queue
end
def process_queue
until @event_queue.empty?
event = @event_queue.shift
@subscribers[event.type].each do |handler|
result = handler.call(event)
@event_queue << result if result.is_a?(Event)
end
end
end
end
class Event
attr_reader :type, :data, :timestamp
def initialize(type, data = {})
@type = type
@data = data
@timestamp = Time.now
end
end
# Usage
mediator = EventDrivenMediator.new
mediator.subscribe(:user_registered) do |event|
puts "Sending welcome email to #{event.data[:email]}"
Event.new(:email_sent, recipient: event.data[:email])
end
mediator.subscribe(:email_sent) do |event|
puts "Logging email delivery to #{event.data[:recipient]}"
nil
end
mediator.publish(Event.new(:user_registered, email: 'user@example.com'))
State-Based Mediation
Mediators maintaining state enable context-dependent coordination. The mediator's current state determines how it handles colleague messages:
class StatefulMediator
def initialize
@state = :idle
@context = {}
end
def notify(sender, message)
case @state
when :idle
handle_idle_message(sender, message)
when :processing
queue_message(sender, message)
when :error
reject_message(sender, message)
end
end
private
def handle_idle_message(sender, message)
@state = :processing
@context[:current_operation] = message[:operation]
process_message(sender, message)
@state = :idle
process_queued_messages
end
def queue_message(sender, message)
@message_queue ||= []
@message_queue << [sender, message]
end
end
Registry-Based Mediation
For systems with dynamic colleague registration, registry-based mediators track available components and route messages accordingly:
class RegistryMediator
def initialize
@registry = {}
@capabilities = Hash.new { |h, k| h[k] = [] }
end
def register(component, capabilities: [])
id = generate_id
@registry[id] = component
capabilities.each do |capability|
@capabilities[capability] << id
end
component.mediator = self
component.id = id
id
end
def unregister(component_id)
@registry.delete(component_id)
@capabilities.each { |_, ids| ids.delete(component_id) }
end
def request(capability, message)
component_ids = @capabilities[capability]
return nil if component_ids.empty?
component_id = select_component(component_ids, capability)
@registry[component_id].handle(message)
end
def broadcast(capability, message)
@capabilities[capability].each do |id|
@registry[id].handle(message)
end
end
private
def select_component(component_ids, capability)
# Load balancing, round-robin, or other selection strategy
component_ids.sample
end
def generate_id
SecureRandom.uuid
end
end
Mediator with Validation Chain
Complex coordination often requires validation chains where multiple checks must pass before action occurs:
class ValidatingMediator
def initialize
@validators = []
@handlers = []
end
def add_validator(&block)
@validators << block
end
def add_handler(&block)
@handlers << block
end
def process(sender, message)
@validators.each do |validator|
result = validator.call(sender, message)
return { success: false, error: result } unless result == true
end
responses = @handlers.map { |handler| handler.call(sender, message) }
{ success: true, responses: responses }
end
end
# Usage
mediator = ValidatingMediator.new
mediator.add_validator do |sender, message|
message[:content].length <= 1000 || "Message too long"
end
mediator.add_validator do |sender, message|
sender.permissions.include?(:post) || "Insufficient permissions"
end
mediator.add_handler do |sender, message|
Database.save(message)
end
mediator.add_handler do |sender, message|
NotificationService.notify(message[:recipients])
end
Common Pitfalls
Implementing the Mediator Pattern incorrectly leads to maintenance problems and architectural issues that negate the pattern's benefits.
Monolithic Mediator
The most common mistake involves creating mediators that handle too many responsibilities. As systems grow, developers add coordination logic to existing mediators rather than creating new ones, resulting in thousand-line mediators that become maintenance nightmares:
# Anti-pattern: Monolithic mediator
class SystemMediator
def notify(sender, event)
case event[:type]
when :user_action then handle_user_action(event)
when :database_event then handle_database_event(event)
when :network_event then handle_network_event(event)
when :ui_event then handle_ui_event(event)
when :file_event then handle_file_event(event)
# ... hundreds more event types
end
end
# ... hundreds of private methods
end
# Better: Specialized mediators
class UserActionMediator
# Handles only user-related coordination
end
class DataAccessMediator
# Handles only database-related coordination
end
Mediator size should remain proportional to the complexity of coordination it manages. Mediators exceeding 200-300 lines warrant examination for splitting opportunities. Creating multiple mediators organized by domain or subsystem maintains the benefits of centralized coordination without monolithic complexity.
Colleague Knowledge of Mediator Details
Colleagues should remain agnostic about mediator implementation details. When colleagues know too much about mediator structure or behavior, the coupling supposedly eliminated by the pattern reappears in a different form:
# Anti-pattern: Colleague knows mediator details
class BadColleague
def perform_action
if @mediator.is_a?(ChatMediator)
@mediator.send_message(self, @message)
elsif @mediator.is_a?(NotificationMediator)
@mediator.notify_users(self, @message)
end
end
end
# Better: Uniform interface
class GoodColleague
def perform_action
@mediator.notify(self, type: :message, content: @message)
end
end
Colleagues should interact with mediators through abstract interfaces or protocols. Type-checking mediator implementations or calling mediator-specific methods indicates poor abstraction. The mediator interface should remain stable even as mediator implementations change.
Insufficient Encapsulation
Mediators must encapsulate coordination logic completely. Leaking coordination logic into colleagues defeats the pattern's purpose:
# Anti-pattern: Coordination logic in colleague
class Component
def update_value(value)
@value = value
# Should be in mediator
if value > 100
@mediator.other_components.each do |comp|
comp.set_warning(true)
end
end
end
end
# Better: Pure delegation
class Component
def update_value(value)
@value = value
@mediator.notify(self, event: :value_changed, value: value)
end
end
class Mediator
def notify(sender, event:, value:)
if event == :value_changed && value > 100
warn_other_components(sender)
end
end
end
Colleagues should focus exclusively on their individual responsibilities. Any logic determining how components interact belongs in the mediator. Coordination logic appearing in colleague classes suggests incomplete mediation.
Bypassing the Mediator
Systems sometimes evolve to allow direct colleague-to-colleague communication, undermining the pattern's benefits. This occurs gradually as developers add "just this one" direct reference for perceived efficiency:
# Anti-pattern: Direct colleague reference
class UserInterface
def initialize(mediator, data_store)
@mediator = mediator
@data_store = data_store # Direct reference
end
def save
@data_store.save(@data) # Bypasses mediator
end
end
# Better: All communication through mediator
class UserInterface
def initialize(mediator)
@mediator = mediator
end
def save
@mediator.notify(self, event: :save_requested, data: @data)
end
end
Once colleagues maintain direct references, the coupling returns. Maintaining discipline to route all communication through the mediator preserves architectural integrity. Performance optimization should target mediator implementation, not bypass the mediator.
Missing Mediator Abstraction
Concrete mediator classes without abstract interfaces limit flexibility and testability:
# Problem: Concrete mediator, no interface
class ConcreteMediator
def coordinate(sender, message)
# Implementation
end
end
class Component
def initialize(mediator)
@mediator = mediator # Depends on concrete class
end
end
# Better: Abstract interface
class Mediator
def coordinate(sender, message)
raise NotImplementedError
end
end
class ChatMediator < Mediator
def coordinate(sender, message)
# Implementation
end
end
class MockMediator < Mediator
def coordinate(sender, message)
# Test implementation
end
end
Abstract mediator interfaces enable testing colleagues with mock mediators and swapping mediator implementations without modifying colleagues. This abstraction proves essential for unit testing and system evolution.
Synchronous Blocking
Mediators processing messages synchronously can create performance bottlenecks and deadlocks in concurrent systems:
# Problem: Synchronous processing blocks
class SyncMediator
def notify(sender, message)
colleagues.each do |colleague|
colleague.handle(message) # Blocks if colleague is slow
end
end
end
# Better: Asynchronous processing
class AsyncMediator
def initialize
@queue = Queue.new
@worker = Thread.new { process_queue }
end
def notify(sender, message)
@queue << [sender, message]
end
private
def process_queue
loop do
sender, message = @queue.pop
colleagues.each { |c| c.handle(message) }
end
end
end
For systems with high message throughput or slow colleague operations, asynchronous mediation prevents blocking. Queue-based implementations enable continued operation even when individual colleagues lag.
Reference
Core Components
| Component | Responsibility | Key Methods |
|---|---|---|
| Mediator | Defines coordination interface | notify, register, coordinate |
| ConcreteMediator | Implements coordination logic | Process colleague notifications, manage colleague references |
| Colleague | Defines colleague interface | send, receive, notify mediator |
| ConcreteColleague | Implements specific behavior | Business logic, mediator notification |
Communication Patterns
| Pattern | Description | Use Case |
|---|---|---|
| Broadcast | Mediator sends to all colleagues | System announcements, state synchronization |
| Unicast | Mediator sends to specific colleague | Directed messages, command execution |
| Filtered | Mediator sends based on criteria | Selective notification, permission-based routing |
| Queued | Messages queued for processing | Async systems, load management |
| Priority | Messages processed by priority | Critical events, SLA enforcement |
Implementation Strategies
| Strategy | Advantages | Disadvantages |
|---|---|---|
| Class-based | Clear structure, type safety | More boilerplate, less flexible |
| Block-based | Concise, flexible | Less explicit structure |
| Module-based | Reusable, composable | Additional complexity |
| Observer integration | Familiar pattern | One-way communication focus |
| Event-driven | Decoupled, scalable | Harder to trace execution flow |
| Registry-based | Dynamic components | Runtime overhead |
When to Use Mediator
| Scenario | Use Mediator | Direct Communication |
|---|---|---|
| Complex interdependencies | Yes | No |
| Simple two-way communication | No | Yes |
| Frequently changing interactions | Yes | No |
| Stable relationships | No | Yes |
| Need centralized control | Yes | No |
| Performance critical path | Maybe | Likely |
| Dynamic component composition | Yes | No |
| Fixed small component set | No | Yes |
Common Message Types
| Type | Purpose | Data Included |
|---|---|---|
| Notification | Inform of state change | sender, event type, new state |
| Request | Ask for action | sender, action type, parameters |
| Response | Reply to request | status, result, error info |
| Query | Ask for information | query type, criteria |
| Command | Direct action | command type, target, params |
| Event | Report occurrence | event type, timestamp, context |
Testing Strategies
| Aspect | Approach |
|---|---|
| Mediator isolation | Mock colleagues, test coordination logic independently |
| Colleague isolation | Mock mediator, test colleague behavior |
| Integration | Real components, verify end-to-end coordination |
| Message flow | Trace message routing, verify delivery |
| Error handling | Inject failures, verify recovery behavior |
| Concurrency | Simulate concurrent access, verify thread safety |
| Performance | Measure throughput, latency under load |
Ruby-Specific Patterns
| Pattern | Implementation |
|---|---|
| Block-based mediator | EventMediator.new { on(:event) { handler } } |
| Module mixing | include Mediates; mediates :colleague_type |
| Observable integration | require 'observer'; include Observable |
| Thread-safe queue | Queue.new for async message processing |
| Dynamic registration | method_missing for flexible colleague addition |
| Proc handlers | Store handlers as procs/lambdas |
Mediator Size Guidelines
| Lines of Code | Assessment | Action |
|---|---|---|
| 0-100 | Appropriate | Continue |
| 100-200 | Monitor | Watch for growth |
| 200-300 | Review | Consider splitting |
| 300+ | Too large | Split into multiple mediators |
Anti-Pattern Detection
| Symptom | Anti-Pattern | Solution |
|---|---|---|
| Thousand-line mediator | Monolithic mediator | Split by domain |
| Type checking mediator | Insufficient abstraction | Define abstract interface |
| Logic in colleagues | Leaking coordination | Move to mediator |
| Direct colleague references | Bypassing mediator | Remove references |
| Blocking operations | Synchronous bottleneck | Implement async queue |
| Complex nested conditions | Poor organization | Extract coordination methods |