Bridge
Decouple an abstraction from its implementation so that the two can vary independently.
Also known as: Handle/Body
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Abstraction | Panel | Defines what's drawn, and holds a reference to the terminal that draws it. |
| Refined Abstraction | PurseLine, WaveBanner | Kinds of panel. They supply the text and colour. |
| Implementor | #write(text, colour:) | The low-level interface every terminal shares. |
| Concrete Implementor | AnsiTerminal, PlainTerminal | Does the actual writing. |
Ruby implementation
# 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
endIn 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.
@terminalis the bridge. Composition replaces the class explosion you would get fromAnsiPurseLine,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 == :ansibranch. 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.