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

RoleIn this exampleResponsibility
MediatorBattlefieldKnows every tower and enemy, and holds the rules for targeting and rewards.
ColleagueTower, EnemyKnows only the battlefield. It reports to it and is called back by it.

Ruby implementation

mediator.rb
# 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
end

In 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) in Tower#initialize means a tower is on the battlefield from the moment it exists.
  • or return ends #fire early when there’s no target, without an extra if block.
  • Keep colleagues thin. Tower#fire is one line that hands itself to the mediator. The interesting decisions all live in Battlefield.

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.