Skip to main content

Bridge

complexitypopularity

Split a class that varies along two independent axes into two hierarchies - control and platform - joined by a reference, so each can grow without touching the other.

The problem

Start with a Shape class and two subclasses, Circle and Square. Now the product owner wants colors, so you add Red and Blue. Except you cannot simply add two classes: you need RedCircle, BlueCircle, RedSquare, BlueSquare. Add a triangle and you owe two more. Add green and you owe three more. The hierarchy grows like a multiplication table because you are trying to vary along two independent dimensions with a mechanism that only has one.

The grown-up version is a cross-platform app: several GUIs (customer, admin) times several operating systems (Windows, Linux, macOS), usually held together by conditionals sprayed across the codebase.

The solution

Pull one dimension out into its own hierarchy and have the original class hold a reference to it. Colors become a Color hierarchy; Shape gets a color field and delegates anything color-shaped to it. Adding a color stops touching shapes, and adding a shape stops touching colors.

The GoF names for the two halves are abstraction (the high-level control layer, which should do no real work) and implementation (the platform layer that does). Ignore the academic ring: think remote control and device. The abstraction usually declares rich operations built from the primitive ones the implementation offers.

Client codeAdvancedRemoteControlRadio (Device)new Radio()1new AdvancedRemoteControl(radio)2togglePower()3isEnabled()4enable()5mute()6setVolume(0)7
  1. 1The client picks a platform. This is the only moment anyone names a concrete implementation.
  2. 2The chosen device is handed to the remote constructor. That reference is the bridge.
  3. 3From here on the client talks only to the abstraction. It has forgotten what is on the other end.
  4. 4The remote asks a primitive question through the Device interface, never through the Radio class.
  5. 5One high-level intent, "toggle", becomes a decision plus a low-level call. That split is the whole pattern.
  6. 6A refined-abstraction feature that lives purely in the control hierarchy.
  7. 7Mute is expressed with primitives every device already supports, so no device needed changing to gain it.

Structure

Four moving parts: the abstraction with its control logic, refined abstractions for control variants, the implementation interface listing primitives, and concrete implementations holding platform-specific code. The client links one of each and then speaks only to the abstraction.

«interface»DeviceisEnabled()enable()disable()getVolume()setVolume(pct)getChannel()setChannel(n)IMPLEMENTATIONRemoteControldevice: DevicetogglePower()volumeUp()channelUp()ABSTRACTIONAdvancedRemoteControlmute()REFINEDTvenable()setVolume(pct)setChannel(n)CONCRETERadioenable()setVolume(pct)setChannel(n)CONCRETE
implementsextends

Code

Remotes on one side, devices on the other, one reference field in between.

// One hierarchy trying to vary in two directions at once.
class RemoteControl is
// ...
 
class TvRemote extends RemoteControl is
// ...
class RadioRemote extends RemoteControl is
// ...
class AdvancedTvRemote extends RemoteControl is
// ...
class AdvancedRadioRemote extends RemoteControl is
// ...
 
// Add a Speaker device: two new classes.
// Add a TouchscreenRemote variant: three new classes.
// Add both: eight boxes on the diagram and every one of them
// re-implements the same power/volume/channel logic.
class AdvancedTouchscreenSpeakerRemote extends RemoteControl is
// and here we admit defeat
// Two hierarchies, one reference between them.
class RemoteControl is
protected field device: Device // the bridge
 
method togglePower() is
if (device.isEnabled()) then device.disable() else device.enable()
 
class AdvancedRemoteControl extends RemoteControl is
method mute() is
device.setVolume(0)
 
interface Device is
method enable()
method disable()
method setVolume(percent)
// ...primitives only
 
// Every remote now works with every device, including the ones
// nobody has written yet:
remote = new AdvancedRemoteControl(new Radio())
// M remotes plus N devices, not M times N.
// The "abstraction" is the control half. It keeps a reference to an
// object from the other hierarchy and delegates all real work to it.
class RemoteControl is
protected field device: Device
 
constructor RemoteControl(device: Device) is
this.device = device
 
method togglePower() is
if (device.isEnabled()) then
device.disable()
else
device.enable()
 
method volumeDown() is
device.setVolume(device.getVolume() - 10)
 
method volumeUp() is
device.setVolume(device.getVolume() + 10)
 
method channelDown() is
device.setChannel(device.getChannel() - 1)
 
method channelUp() is
device.setChannel(device.getChannel() + 1)
 
// Control variants extend this side only. No device is affected.
class AdvancedRemoteControl extends RemoteControl is
method mute() is
device.setVolume(0)
 
// The "implementation" interface lists primitives, not features.
// It does not have to mirror the abstraction's methods, and usually
// should not.
interface Device is
method isEnabled()
method enable()
method disable()
method getVolume()
method setVolume(percent)
method getChannel()
method setChannel(channel)
 
// Platform variants extend this side only. No remote is affected.
class Tv implements Device is
// ...
 
class Radio implements Device is
// ...
 
// The client is the matchmaker: it links one side to the other once.
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()
 
radio = new Radio()
remote = new AdvancedRemoteControl(radio)
remote.mute()

When to use it

  • A monolithic class carries several variants of the same functionality - one class that must work against MySQL, Postgres and SQLite, for instance - and every change risks the others.
  • You need to extend a class along two or more orthogonal dimensions. Give each dimension its own hierarchy and let the original delegate.
  • You want to swap implementations at runtime. Assigning a new value to the reference field is the whole operation.

Pitfalls

  • Bridging a cohesive class. If the class genuinely has one axis of variation, the split adds indirection and subtracts clarity.
  • A mirror-image implementation interface. If every abstraction method delegates one-to-one, the implementation layer is decoration, not design. Push primitives down and compose them up.
  • Leaking the platform. The moment the abstraction type-checks for Tv or reads a device-specific field, the two hierarchies are welded together again.

Don't confuse it with

  • Strategy. Same diagram, different problem. Strategy plugs one interchangeable algorithm into one class. Bridge cuts a class in half so two hierarchies can evolve on their own schedules. If you can only extend one side, it is Strategy.
  • Adapter. Bridge is designed before the collision happens; Adapter is repair work after it. Bridge partners are built to fit; Adapter partners never were.
  • State. State also delegates to a swappable object, but state objects know the other states and hand control between them. Bridge implementations are peers that never mention each other.
  • Abstract Factory. Frequently paired with Bridge rather than confused with it: when only certain abstraction and implementation combinations are legal, a factory can encapsulate the matchmaking and keep the client out of it.

Check yourself

Question 1 of 5

You have 4 report layouts and 5 storage backends. What does Bridge do to the class count?