The Bridge is a pattern that separates an abstraction from its implementation so that both can evolve independently. The definition sounds academic, but the problem it solves is quite concrete.
What does this actually mean?
It means that sometimes inheritance plays a nasty trick on us. Sometimes, we try to classify objects by two different criteria at once (e.g., shape and color, or operating system and window type), and we end up with an explosion of class combinations.
The Bridge comes to break that rigid inheritance and replace it with composition, creating two separate hierarchies connected by a “bridge.”
The Problem: The Multiplication of Classes
Imagine we are developing a cross-platform graphics engine.
We have two concepts:
- Geometric Shapes: Circle, Square.
- Rendering APIs: DirectX (Windows), OpenGL (Linux/Mac).
If we use traditional inheritance, we would start by creating a base class Shape. Then we would inherit Circle and Square.
But now we have to support the APIs. So we end up with:
CircleDirectXCircleOpenGLSquareDirectXSquareOpenGL
We have 4 classes.
If tomorrow we add a new shape (Triangle), we need to create 2 more classes (one for each API).
And if we add a new API (Vulkan), we need to create 3 more classes (one for each shape).
The number of classes grows multiplicatively: Shapes x APIs. This is unsustainable.
The Solution: Divide and Conquer
The Bridge pattern proposes to stop trying to do everything in a single hierarchy. Instead, we create two separate hierarchies:
- Abstraction (The “What”): The hierarchy of Shapes (
Circle,Square). - Implementation (The “How”): The hierarchy of Rendering (
DirectX,OpenGL).
And the Shape class will have a reference (a bridge) to an object of type Renderer.
This way, if we add a new shape, we don’t touch the renderers. And if we add a new renderer, we don’t touch the shapes. The growth changes from multiplicative to additive.
Implementation in C#
Let’s implement our simplified graphics engine.
The Implementation Interface (The “How”)
First, we define the basic operations that any graphics engine should know how to perform.
// Implementor
public interface IRenderer
{
void RenderCircle(float radius);
void RenderSquare(float side);
}
// Concrete Implementor A
public class DirectXRenderer : IRenderer
{
public void RenderCircle(float radius)
{
Console.WriteLine($"[DirectX] Drawing Circle of radius {radius} using Windows shaders.");
}
public void RenderSquare(float side)
{
Console.WriteLine($"[DirectX] Drawing Square of side {side} using Windows shaders.");
}
}
// Concrete Implementor B
public class OpenGLRenderer : IRenderer
{
public void RenderCircle(float radius)
{
Console.WriteLine($"[OpenGL] Drawing Circle of radius {radius} in a graphics buffer.");
}
public void RenderSquare(float side)
{
Console.WriteLine($"[OpenGL] Drawing Square of side {side} in a graphics buffer.");
}
}The Abstraction (The “What”)
Now we define the shapes. Notice that the Shape class receives the IRenderer in the constructor. This is the bridge.
// Abstraction
public abstract class Shape
{
protected IRenderer _renderer;
protected Shape(IRenderer renderer)
{
_renderer = renderer;
}
public abstract void Draw();
public abstract void Scale(float percentage);
}The Refined Abstractions
The concrete shapes use the bridge to draw themselves, but they add their own high-level logic.
// Refined Abstraction
public class Circle : Shape
{
private float _radius;
public Circle(IRenderer renderer, float radius) : base(renderer)
{
_radius = radius;
}
public override void Draw()
{
// Delegate the heavy lifting to the implementor
_renderer.RenderCircle(_radius);
}
public override void Scale(float percentage)
{
_radius *= percentage;
}
}
public class Square : Shape
{
private float _side;
public Square(IRenderer renderer, float side) : base(renderer)
{
_side = side;
}
public override void Draw()
{
_renderer.RenderSquare(_side);
}
public override void Scale(float percentage)
{
_side *= percentage;
}
}The Client (Mixing and Matching)
Now we can combine any shape with any rendering engine at runtime.
class Program
{
static void Main(string[] args)
{
IRenderer windows = new DirectXRenderer();
IRenderer linux = new OpenGLRenderer();
// Circle on Windows
Shape circleWin = new Circle(windows, 5.0f);
circleWin.Draw();
// The SAME Circle object type, but on Linux
Shape circleLinux = new Circle(linux, 5.0f);
circleLinux.Draw();
// Square on Windows
Shape squareWin = new Square(windows, 10.0f);
squareWin.Draw();
}
}Output:
[DirectX] Drawing Circle of radius 5 using Windows shaders.
[OpenGL] Drawing Circle of radius 5 in a graphics buffer.
[DirectX] Drawing Square of side 10 using Windows shaders.Notice the power of this: The Circle class has no idea whether it is running on DirectX or OpenGL. It only knows it has a _renderer that knows how to draw.
Bridge vs Adapter vs Strategy
It’s easy to get confused, so let’s clarify:
- Adapter: Makes things that already exist and are incompatible work together. It is used a posteriori.
- Bridge: It is designed in advance. It serves to structure the system and avoid massive inheritance when we have orthogonal (independent) dimensions of variation.
- Strategy: It looks very similar structurally (an object delegating to an interface). But Strategy is used to change a specific algorithm or behavior (the “how to do something”), while Bridge is an architectural structure to separate a complete hierarchy.
When to use Bridge?
- Independence: When you want the implementation to be able to change at runtime (like changing light/dark visual themes in a UI).
- Class explosion: when you create subclasses to cover all possible combinations (
RedCar,BlueCar,RedMotorcycle,BlueMotorcycle…). It can be more flexible to separateVehicleandColor. - Parallel development: One team can work on Shapes (business logic) and another on Renderers (low-level drivers) without stepping on each other, as they only depend on the interfaces.
The Bridge pattern is a lifesaver for long-term architecture. It allows us to keep our high-level classes clean and agnostic of the technical implementation details.