Bridge Pattern
The Bridge Pattern separates an abstraction from its implementation so that both can vary independently. It's a structural design pattern that helps you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other.
Think of it as building a bridge between two sides: abstractions (what the client sees) and implementations (how things are done behind the scenes).
Problem
Imagine you're developing a shape rendering system. You have different shapes (circle, square) and different rendering methods (vector, raster). If you create a class for each combination, you end up with an explosion of classes: VectorCircle, RasterCircle, VectorSquare, RasterSquare, etc. Adding a new shape or rendering method requires adding multiple new classes.
Components
- Abstraction: Defines the interface for the "control" part of two class hierarchies and maintains a reference to the Implementation.
- Refined Abstraction: Extends the Abstraction interface.
- Implementation: Defines the interface for the "implementation" part of the class hierarchies.
- Concrete Implementation: Implements the Implementation interface.
Implementation Example
Let's implement a drawing application that can draw different shapes using different rendering methods:
// Implementation interface
class Renderer {
renderCircle(radius) {
// This method should be implemented by concrete renderers
}
renderSquare(side) {
// This method should be implemented by concrete renderers
}
}
// Concrete Implementations
class VectorRenderer extends Renderer {
renderCircle(radius) {
return `Drawing a circle of radius ${radius} using vector graphics`;
}
renderSquare(side) {
return `Drawing a square with side ${side} using vector graphics`;
}
}
class RasterRenderer extends Renderer {
renderCircle(radius) {
return `Drawing a circle of radius ${radius} using raster pixels`;
}
renderSquare(side) {
return `Drawing a square with side ${side} using raster pixels`;
}
}
// Abstraction
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() {
// This method should be implemented by refined abstractions
}
resize(percentage) {
// This method should be implemented by refined abstractions
}
}
// Refined Abstractions
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
return this.renderer.renderCircle(this.radius);
}
resize(percentage) {
this.radius *= percentage / 100;
return this;
}
}
class Square extends Shape {
constructor(renderer, side) {
super(renderer);
this.side = side;
}
draw() {
return this.renderer.renderSquare(this.side);
}
resize(percentage) {
this.side *= percentage / 100;
return this;
}
}
With this pattern, we can:
- Add new shapes without changing the rendering code
- Add new rendering methods without changing the shape code
- Create any combination of shapes and renderers
Interactive Demo: Shape Rendering Bridge
Experiment with different shapes and rendering methods to see the Bridge pattern in action.
When to Use
- When you want to avoid a permanent binding between an abstraction and its implementation
- When both the abstractions and their implementations should be extensible through subclasses
- When changes in the implementation should not impact the client code
- When you have a proliferation of classes resulting from multiple orthogonal dimensions of variation
Benefits
- Decouples interface from implementation
- Improves extensibility (can extend both abstractions and implementations independently)
- Hides implementation details from the client
- Follows the Open/Closed Principle: open for extension, closed for modification
Real-World Uses
- GUI frameworks that separate visual elements from platform-specific code
- Database access layers that decouple business logic from database implementation
- Device drivers that separate device-independent code from hardware-specific code
- Cross-platform applications that need to function on multiple operating systems