Structural Design Patterns
Imagine you are developing a graphics application. You have a set of Shape classes, like Circle and Square. You also need to support rendering these shapes on different operating systems (e.g., Windows, macOS), which have different drawing APIs.
If you try to model this with inheritance, you'll face a combinatorial explosion. You'll need a class for every possible combination:
WindowsCircle, MacCircleWindowsSquare, MacSquareThis hierarchy is already messy with just two shapes and two operating systems. Now, what happens if you add a new shape, like a Triangle? You have to add a WindowsTriangle and a MacTriangle. What if you add a new rendering target, like Linux? You have to add a LinuxCircle and a LinuxSquare.
Your class hierarchy is growing exponentially in two independent dimensions: the shape's abstraction (what it is) and its rendering implementation (how it's drawn). This is a maintenance nightmare.
The Bridge is a structural design pattern that solves this problem by decoupling an abstraction from its implementation so that the two can vary independently.
Instead of one giant class hierarchy, the Bridge pattern splits it into two separate, independent hierarchies:
Circle, Square).WindowsRenderer, MacRenderer).The "Bridge" is created by having the Abstraction object hold a reference to an Implementation object. The client interacts with the Abstraction, which in turn delegates the low-level "implementation" work to the implementation object it holds.
Shape class). It holds a reference to an Implementor.Abstraction (e.g., Circle, Square).IRenderer interface).WindowsRenderer, MacRenderer).Let's build our Shape application using the Bridge pattern.
Step 1: The Implementation Hierarchy This hierarchy deals only with the low-level details of drawing.
// The Implementor interface
public interface IRenderer {
void renderCircle(float radius);
void renderSquare(float side);
}
// Concrete Implementors
public class WindowsRenderer implements IRenderer {
@Override public void renderCircle(float radius) { /* Use Windows GDI API... */ }
@Override public void renderSquare(float side) { /* Use Windows GDI API... */ }
}
public class MacRenderer implements IRenderer {
@Override public void renderCircle(float radius) { /* Use macOS Core Graphics API... */ }
@Override public void renderSquare(float side) { /* Use macOS Core Graphics API... */ }
}
Step 2: The Abstraction Hierarchy This hierarchy deals only with the high-level logic of what a shape is.
// The Abstraction
public abstract class Shape {
// The "Bridge" is this reference to the implementor.
protected IRenderer renderer;
public Shape(IRenderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
// Refined Abstractions
public class Circle extends Shape {
private float radius;
public Circle(IRenderer renderer, float radius) {
super(renderer);
this.radius = radius;
}
@Override public void draw() {
// High-level logic delegates to the low-level implementor
renderer.renderCircle(radius);
}
}
public class Square extends Shape {
private float side;
public Square(IRenderer renderer, float side) {
super(renderer);
this.side = side;
}
@Override public void draw() {
renderer.renderSquare(side);
}
}
Step 3: The Client Code The client can now create any combination of shape and renderer independently.
IRenderer windows = new WindowsRenderer();
IRenderer mac = new MacRenderer();
// Create any combination at runtime
Shape macCircle = new Circle(mac, 5);
Shape windowsSquare = new Square(windows, 10);
macCircle.draw();
windowsSquare.draw();
Now you can add a Triangle shape without touching any renderer code, or add a LinuxRenderer without touching any shape code.
Benefits:
Shape abstraction and is completely shielded from the platform-specific rendering details.Drawbacks:
The Bridge pattern is a powerful, strategic tool for managing complex dependencies in a system.