Bridge Pattern is a structural design pattern. As per the Gang of Four (GoF) book “Design Patterns: Elements of Reusable Object-Oriented Software,” the statement describing the Bridge pattern is as follows:
“The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently.”
This statement succinctly captures the essence of the Bridge pattern, emphasizing its goal of separating the abstraction (high-level interface) from its implementation (low-level details) to enable them to change and evolve independently. By using the Bridge pattern, you can achieve greater flexibility, extensibility, and code reusability in your software design.
Problem
Say you have a geometric Shape
class with a pair of subclasses: Circle
and Square
. You want to extend this class hierarchy to incorporate colors, so you plan to create Red
and Blue
shape subclasses. However, since you already have two subclasses, you’ll need to create four class combinations such as BlueCircle
and RedSquare
.
As we add new shape types and colors to the hierarchy, its size grows exponentially. For instance, adding a triangle shape necessitates two subclasses for each color. Similarly, introducing a new color demands three subclasses for each shape type. This escalation worsens as we proceed.
Solution
The Bridge pattern addresses this issue by replacing inheritance with object composition. It involves separating one dimension into a distinct class hierarchy, where the original classes reference an object from this new hierarchy. By doing so, the state and behaviors are no longer confined within a single class.
Implementation
First, let’s define the Color
interface:
public interface Color {
void applyColor();
}
JavaNext, we can implement the concrete color classes:
public class Red implements Color {
@Override
public void applyColor() {
System.out.println("Applying red color");
}
}
public class Green implements Color {
@Override
public void applyColor() {
System.out.println("Applying green color");
}
}
public class Blue implements Color {
@Override
public void applyColor() {
System.out.println("Applying blue color");
}
}
JavaNow, let’s define the Shape
abstraction and its concrete implementations:
public abstract class Shape {
protected Color color;
public Shape(Color color) {
this.color = color;
}
public abstract void draw();
}
public class Circle extends Shape {
public Circle(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("Drawing a circle");
color.applyColor();
}
}
public class Square extends Shape {
public Square(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("Drawing a square");
color.applyColor();
}
}
JavaNow, we can use the Shape
and Color
classes as follows:
public class Application {
public static void main(String[] args) {
Color red = new Red();
Color green = new Green();
Color blue = new Blue();
Shape circle = new Circle(red);
circle.draw();
Shape square = new Square(green);
square.draw();
Shape triangle = new Triangle(blue);
triangle.draw();
}
}
/** OUTPUT
Drawing a circle
Applying red color
Drawing a square
Applying green color
Drawing a triangle
Applying blue color
*/
Javaclass diagram
This demonstrates how the Bridge pattern separates the abstraction (Shape
) from its implementation (Color
), allowing them to vary independently. We can create new shapes and colors without changing the existing classes, promoting code reusability and flexibility.
When to use bridge pattern
- When you want to decouple an abstraction from its implementation, allowing them to vary independently.
- When you have a class hierarchy with multiple dimensions of variation, and you want to avoid the exponential growth of subclasses.
- When you want to extend or add new functionalities to an existing abstraction without modifying its implementation.
- When you need to switch or use different implementations of an abstraction at runtime.
- When you want to promote code reusability by separating higher-level abstractions from lower-level implementation details.
- When you want to establish a clear separation between an interface and its implementation to achieve flexibility and maintainability.
- When you want to build a system that can accommodate future extensions or changes without affecting the existing codebase.