Bridge Pattern: Decoupling Abstraction and Implementation

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.

Bridge Pattern 1 - neatcode

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.

Bridge Pattern - Neatcode

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.

Bridge Pattern - Neatcode

Implementation

First, let’s define the Color interface:

public interface Color {
    void applyColor();
}
Java

Next, 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");
    }
}
Java

Now, 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();
    }
}
Java

Now, 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
*/
Java

class diagram

Bridge Pattern class 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

  1. When you want to decouple an abstraction from its implementation, allowing them to vary independently.
  2. When you have a class hierarchy with multiple dimensions of variation, and you want to avoid the exponential growth of subclasses.
  3. When you want to extend or add new functionalities to an existing abstraction without modifying its implementation.
  4. When you need to switch or use different implementations of an abstraction at runtime.
  5. When you want to promote code reusability by separating higher-level abstractions from lower-level implementation details.
  6. When you want to establish a clear separation between an interface and its implementation to achieve flexibility and maintainability.
  7. When you want to build a system that can accommodate future extensions or changes without affecting the existing codebase.