Bridge

Decouple an abstraction from its implementation so that the two can vary independently.

Also known as: Handle/Body

Participants

RoleIn this exampleResponsibility
AbstractionPanelDefines what's drawn, and holds a reference to the terminal that draws it.
Refined AbstractionPurseLine, WaveBannerKinds of panel. They supply the text and colour.
Implementor#write(text, colour:)The low-level interface every terminal shares.
Concrete ImplementorAnsiTerminal, PlainTerminalDoes the actual writing.

Ruby implementation

bridge.rb
# Implementors: the "how". Each terminal knows only how to write coloured text.
class AnsiTerminal
  COLOURS = { yellow: 33, red: 31 }.freeze

  def write(text, colour:) = "\e[#{COLOURS.fetch(colour)}m#{text}\e[0m"
end

class PlainTerminal
  def write(text, colour:) = text
end

# The Abstraction: the "what". It holds a terminal and talks to it through
# the terminal's interface, never its class.
class Panel
  def initialize(terminal)
    @terminal = terminal
  end

  def render
    @terminal.write(text, colour:)
  end

  private

  def text
    raise NotImplementedError, "#{self.class} must define #text"
  end

  def colour = :yellow
end

# Refined Abstractions vary independently of the terminals.
class PurseLine < Panel
  def initialize(terminal, gold:)
    super(terminal)
    @gold = gold
  end

  private

  def text = "Gold: #{@gold}"
end

class WaveBanner < Panel
  def initialize(terminal, wave:)
    super(terminal)
    @wave = wave
  end

  private

  def text = "~ Wave #{@wave} ~"
  def colour = :red
end

In use

PurseLine.new(AnsiTerminal.new, gold: 120).render
# => "\e[33mGold: 120\e[0m"

WaveBanner.new(PlainTerminal.new, wave: 6).render
# => "~ Wave 6 ~"

Two panels × two terminals = four combinations from four small classes. Add a CursesTerminal and every panel can use it at once, with no panel changing.

Notes

Ruby idioms

  • The bridge is an instance variable. @terminal is the bridge. Composition replaces the class explosion you would get from AnsiPurseLine, PlainPurseLine, AnsiWaveBanner, and so on.
  • The implementor is a duck type. Anything with #write(text, colour:) plugs in, including a fake that records output in tests.

Reach for it when

  • Two dimensions of variation are multiplying subclasses, such as panel × terminal.
  • Every method has the same if mode == :ansi branch. That’s a hidden second hierarchy asking to be pulled out.

Watch for

  • Bridge looks like Strategy in code. The difference is intent: Bridge is a structural split planned up front, while Strategy swaps one algorithm.

Two hierarchies joined by one reference, instead of one hierarchy with a class for every combination.