In one sentence: Bridge decouples an abstraction from its implementation so the two can vary independently — replacing inheritance between them with a pointer.
Real-world analogy
A cordless drill and a box of bits are two separate families of product, and they grow on completely separate schedules. The bit family gets a new member — a countersink, a sanding disc, a square-drive screwdriver bit. The tool family gets a new member — an impact driver, a hammer drill. Nobody buys a new drill because they bought a new bit.
What makes that possible is the chuck: every bit ends in the same hex shank, every tool grips that shank the same way. The tool doesn't inherit from the bit or contain a special compartment per bit type. It just holds whatever is currently clamped in and spins it.
Notice the two sides don't speak the same vocabulary. A bit offers one primitive — spin me against material and I'll bore, turn, or abrade. The tool composes richer jobs out of that primitive: drive to a set torque and stop, pulse in short bursts, hammer while it turns. That asymmetry is the point, not an accident.
Mapping the analogy to the pattern:
- The drill body → the Abstraction, which holds a bit rather than being one.
- The impact driver and hammer drill → RefinedAbstraction — richer jobs built from the same primitive, written without naming a single bit.
- The shank standard every bit conforms to → the Implementor interface; each individual bit → a ConcreteImplementor.
- The chuck clamped around the shank → the bridge itself: one reference, resolved when you assemble the tool, not when it was manufactured.
- Buying a sanding disc without buying a drill → the two hierarchies varying independently:
tools + bits, nevertools × bits.
Another angle: a car's steering wheel and pedals. The controls are the same whether there's a petrol engine, a diesel, or an electric motor underneath — and the drivetrain can be redesigned entirely without teaching drivers a new way to sit.
The problem
You start with a Remote class that controls a TV. Then you add a radio. Then a "smart" remote with extra buttons. Handle this with inheritance and you get:
Remote
TvRemote RadioRemote
SmartTvRemote SmartRadioRemote- The class count multiplies. Two remote kinds × two devices = 4 classes. Add a projector and it's 6. Add a "kids" remote and it's 9. This is the class explosion, and it grows as
remotes × devices. - Every new device edits every remote. The two concerns are welded together, so neither can change alone.
- Code gets duplicated.
SmartTvRemoteandSmartRadioRemoteshare all the "smart" logic and can't share an implementation.
GoF's version of this story is a portable Window class that must run on two windowing systems. Subclassing gives you XWindow and PMWindow — then an IconWindow needs XIconWindow and PMIconWindow, and a third platform demands a new subclass for every kind of window. Worse, clients that write new XWindow are now permanently welded to one platform.
The solution
Notice that "what the remote does" and "which device it talks to" are two independent dimensions. Split them:
- Extract the device side into its own interface — the implementation (
Device). - Keep the remote side as the abstraction (
Remote), which holds a pointer to aDevicerather than inheriting from one. - Extend each side on its own. New device: one class. New remote: one class.
The count drops from remotes × devices to remotes + devices. The pointer between them is the "bridge."
A detail that's easy to miss: the two interfaces do not have to mirror each other. GoF's advice is that the Implementor should expose only primitive operations (draw a line, set the volume), while the Abstraction builds higher-level operations on top of those primitives (draw a rectangle, "volume up by ten"). If your two interfaces are method-for-method identical, you probably haven't found the real split yet.
Note: GoF's implementation notes hold two ideas worth stealing. First, the degenerate bridge — even with a single implementation, splitting the class is worth it in C++ purely so the implementation lives in a private header and clients relink rather than recompile. (Carolan called this the "Cheshire Cat"; today's C++ calls it the pimpl idiom.) Second, deciding which implementor to create is a job worth delegating to an Abstract Factory, so the Abstraction isn't coupled to any concrete implementor class. And a warning: you cannot build a true Bridge with multiple inheritance in C++ — inheriting privately from a concrete implementor binds it statically, which is the one thing Bridge exists to avoid.
Structure
Two hierarchies, connected by one reference:
- Abstraction — defines the interface clients use and keeps a reference to an
Implementor. In the example:Remote. - RefinedAbstraction — extends the interface defined by
Abstraction, without knowing which implementor is underneath. In the example:AdvancedRemote. - Implementor — the interface for implementation classes. It typically offers only primitive operations, and needn't resemble
Abstraction's interface at all. In the example:Device. - ConcreteImplementor — a specific implementation of the
Implementorinterface. In the example:TvandRadio. - Client — programs against
Abstractionand never names a concrete implementor.
The collaboration is a single sentence: the Abstraction forwards client requests to its Implementor object.
C++ example
Two remote types and two device types — four working combinations from four classes, not sixteen.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
// --- Implementation side ---
class Device {
public:
virtual ~Device() = default;
virtual std::string name() const = 0;
virtual void setPower(bool on) = 0;
virtual void setVolume(int percent) = 0;
virtual int volume() const = 0;
};
class Tv : public Device {
public:
std::string name() const override { return "TV"; }
void setPower(bool on) override {
std::cout << "TV power " << (on ? "on" : "off") << "\n";
}
void setVolume(int percent) override { volume_ = percent; }
int volume() const override { return volume_; }
private:
int volume_ = 20;
};
class Radio : public Device {
public:
std::string name() const override { return "Radio"; }
void setPower(bool on) override {
std::cout << "Radio power " << (on ? "on" : "off") << "\n";
}
void setVolume(int percent) override { volume_ = percent; }
int volume() const override { return volume_; }
private:
int volume_ = 50;
};
// --- Abstraction side: holds a Device, does not inherit from one ---
class Remote {
public:
explicit Remote(std::unique_ptr<Device> device) : device_(std::move(device)) {}
virtual ~Remote() = default;
void powerOn() { device_->setPower(true); }
void volumeUp() {
device_->setVolume(device_->volume() + 10);
std::cout << device_->name() << " volume: " << device_->volume() << "\n";
}
protected:
std::unique_ptr<Device> device_;
};
// A refined abstraction — new remote, zero new device classes.
class AdvancedRemote : public Remote {
public:
using Remote::Remote;
void mute() {
device_->setVolume(0);
std::cout << device_->name() << " muted\n";
}
};
int main() {
Remote basic(std::make_unique<Tv>());
basic.powerOn();
basic.volumeUp();
AdvancedRemote advanced(std::make_unique<Radio>());
advanced.powerOn();
advanced.volumeUp();
advanced.mute();
}TV power on
TV volume: 30
Radio power on
Radio volume: 60
Radio mutedAdvancedRemote was written without ever mentioning Radio, and Radio was written without ever mentioning a remote — yet they compose at runtime.
When should you use it?
GoF's applicability list, in plain speech. Use Bridge when:
- You want to avoid a permanent binding between an abstraction and its implementation — for instance because the implementation must be chosen, or switched, while the program is running.
- Both sides should be extensible by subclassing. Bridge lets you combine any abstraction with any implementation and grow the two hierarchies separately.
- Changing an implementation must not disturb clients — ideally they shouldn't even have to recompile.
- (C++ specifically) You want to hide the implementation from clients completely. In C++ a class's representation is visible in its header, so the only way to truly conceal it is to move it behind an implementor interface.
- You've noticed a proliferation of classes of the
Xxx × Yyyshape. Rumbaugh calls these "nested generalizations"; they're a signal that one object wants to be two. - You want to share one implementation among several objects — reference-counted, say — and keep that fact hidden from the client.
Note: Don't reach for Bridge on day one. If there's only one implementation and no sign of a second, the extra indirection is mostly cost — the exception being the C++ compile-firewall case above, where it pays for itself immediately.
Pros and cons
Pros (GoF's consequences)
- Interface and implementation are decoupled. The implementation is no longer bound to the interface at compile time; it can be configured at run time, and an object can even swap its implementation mid-life.
- No compile-time dependency on the implementation. Changing an implementor doesn't force a recompile of the abstraction or its clients — which is what makes binary compatibility between library versions possible.
- It encourages layering. The high-level part of the system only ever needs to know about
AbstractionandImplementor, which tends to produce a better-structured system overall. - Improved extensibility. The two hierarchies grow independently: additive, not multiplicative.
- Implementation details stay hidden from clients — including sharing schemes and any reference counting that goes with them.
Cons
- More classes and one extra indirection even in the simple case.
- Getting the abstraction/implementation split wrong is expensive to undo, and a too-fat
Implementorinterface defeats the purpose. - Harder to read for newcomers — the behavior lives in two files instead of one.
- Someone still has to decide which implementor to create; that logic has to live somewhere (constructor, factory, or configuration).
How to remember it
Memory hook: "Two ladders, one rung between them." You can climb either ladder — add remotes, add devices — without touching the other. The confusion to settle is Bridge vs. Adapter, and GoF settles it by intent and timing: Adapter reconciles two interfaces that already exist and already clash, bolted on after the fact; Bridge is drawn on the whiteboard before either hierarchy is written, precisely so they can grow apart. Same picture, opposite stories.
Related patterns
- Abstract Factory — GoF's recommended way to create and configure a particular Bridge, so the abstraction never names a concrete implementor.
- Adapter — aimed at making unrelated classes work together, and usually applied to systems after they're designed; Bridge is used up front.
- Strategy — structurally similar, but swaps a single interchangeable algorithm; Bridge separates entire hierarchies, and the implementor side isn't a drop-in alternative so much as a platform.
- State — also delegates to a held object, but the held object changes as the owner's state changes.