Mediator
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Mediator | Battlefield | Knows every tower and enemy, and holds the rules for targeting and rewards. |
| Colleague | Tower, Enemy | Knows only the battlefield. It reports to it and is called back by it. |
Ruby implementation
# The Mediator: towers and enemies never talk to each other directly.
# The battlefield decides who gets hit and what happens when an enemy falls.
class Battlefield
attr_reader :gold, :log
def initialize
@towers = []
@enemies = []
@gold = 0
@log = []
end
def deploy(tower) = @towers << tower
def spawn(enemy) = @enemies << enemy
def tick
@towers.each(&:fire)
end
# Called by a tower: aim at the enemy in range that is furthest along.
def fire(tower)
in_range = @enemies.select { |enemy| (enemy.position - tower.position).abs <= tower.range }
target = in_range.max_by(&:position) or return
@log << "#{tower.name} hit #{target.name}"
target.hit(tower.damage)
end
# Called by an enemy when its health runs out.
def defeated(enemy)
@enemies.delete(enemy)
@gold += enemy.bounty
@log << "#{enemy.name} defeated, +#{enemy.bounty}g"
end
end
# Colleagues: each one knows only the battlefield.
class Tower
attr_reader :name, :position, :range, :damage
def initialize(name, battlefield, position:, range:, damage:)
@name = name
@battlefield = battlefield
@position = position
@range = range
@damage = damage
battlefield.deploy(self)
end
def fire = @battlefield.fire(self)
end
class Enemy
attr_reader :name, :position, :hp, :bounty
def initialize(name, battlefield, position:, hp:, bounty:)
@name = name
@battlefield = battlefield
@position = position
@hp = hp
@bounty = bounty
battlefield.spawn(self)
end
def hit(damage)
@hp -= damage
@battlefield.defeated(self) unless @hp.positive?
end
endIn use
battlefield = Battlefield.new
Tower.new("Arrow", battlefield, position: 5, range: 3, damage: 9)
Enemy.new("Goblin", battlefield, position: 4, hp: 30, bounty: 4)
Enemy.new("Orc", battlefield, position: 7, hp: 9, bounty: 9)
battlefield.tick
battlefield.log # => ["Arrow hit Orc", "Orc defeated, +9g"]
battlefield.gold # => 9
Towers don’t reach into the enemy list, and enemies don’t know who pays their bounty. Every interaction runs through the battlefield, so new rules (splash damage, slowing, shields) go in one place.
Notes
Ruby idioms
- Colleagues register themselves.
battlefield.deploy(self)inTower#initializemeans a tower is on the battlefield from the moment it exists. or returnends#fireearly when there’s no target, without an extraifblock.- Keep colleagues thin.
Tower#fireis one line that hands itself to the mediator. The interesting decisions all live inBattlefield.
Reach for it when
- A group of objects talk to each other in complicated ways, and every change touches several of them: towers and enemies, form fields, chat participants.
Watch for
- The mediator can become a god object that knows too much. If it starts collecting logic that isn’t about coordination, split it.
- Mediator coordinates two-way interaction. If messages only flow one way, from a subject to listeners, Observer is simpler.
Colleagues only talk to the mediator. The mediator decides what happens.