2.2 Bridge Pattern - Structural Design Pattern

Design patterns are essential to software engineering because they help write more effective, scalable, and maintainable code. The Bridge Pattern is one example of a pattern that belongs to the structural design pattern category. By separating an abstraction from its implementation, the Bridge Pattern allows both to change on their own. This encourages adaptability and makes it possible for changes made to one section of the code to largely affect the other sections.

Understanding the Bridge Pattern

When you need to keep an abstraction and its implementation separate and let them develop separately, the Bridge Pattern comes in handy. It accomplishes this by establishing a bridge between the two distinct hierarchies—one for implementation and another for abstraction.

Components of the Bridge Pattern

  1. Abstraction: This keeps track of an object of type Implementor and defines the abstract interface.

  2. Refined Abstraction: Fine-grained elements are added to the abstraction in order to create Refined Abstraction.

  3. Implementor: This describes the implementation classes' interface. It can offer a different set of operations and need not directly match the abstraction interface.

  4. Concrete Implementor: This defines the actual implementation of the Implementor interface and puts it into practice.

Example: Bridge Pattern in Java

Let's illustrate the Bridge Pattern with a simple example in Java. Consider a scenario where we have different types of shapes (abstraction) and different drawing methods (implementation). We want to keep these concerns separate and provide the flexibility to add new shapes or drawing methods independently.

// Implementor interface
interface DrawingAPI {
    void drawCircle(double x, double y, double radius);
}
// Concrete Implementor 1
class DrawingAPI1 implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("API1.circle at %f:%f radius %f%n", x, y, radius);
    }
}
// Concrete Implementor 2
class DrawingAPI2 implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.printf("API2.circle at %f:%f radius %f%n", x, y, radius);
    }
}
// Abstraction
abstract class Shape {
    protected DrawingAPI drawingAPI;

    protected Shape(DrawingAPI drawingAPI) {
        this.drawingAPI = drawingAPI;
    }
    public abstract void draw();
}
// Refined Abstraction
class Circle extends Shape {
    private double x, y, radius;

    public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    @Override
    public void draw() {
        drawingAPI.drawCircle(x, y, radius);
    }
}
// Client
public class BridgePatternExample {
    public static void main(String[] args) {
        DrawingAPI api1 = new DrawingAPI1();
        DrawingAPI api2 = new DrawingAPI2();

        Shape circle1 = new Circle(1, 2, 3, api1);
        Shape circle2 = new Circle(5, 7, 11, api2);

        circle1.draw();
        circle2.draw();
    }
}

In this example:

  • DrawingAPI serves as the Implementor interface.

  • DrawingAPI1 and DrawingAPI2 are concrete implementations of drawing APIs.

  • Shape serves as the Abstraction.

  • Circle is a refined abstraction.

  • BridgePatternExample demonstrates how we can use different drawing APIs with different shapes without tightly coupling them.

Conclusion:

In software design, the Bridge Pattern is an effective technique for separating abstraction from implementation. It enhances the codebase's flexibility, scalability, and maintainability by letting various parts differ on their own. Taking the Bridge Pattern into consideration while designing systems can result in more modular and extensible solutions, particularly for those with numerous axes of variation.