Bridge Design Pattern - Definition

The Bridge Design Pattern decouples an abstraction from its implementation so that the two can vary independently. We call this design pattern also as Handle/Body. It is a Structural Design Pattern.

Inheritance

Usually, when we have more than one possible implementation, we will create an abstract class or an interface to declare the common operations. Then, we will have the different implementations extend or implement the abstract class or the interface. The problem with this approach is it that the inheritance binds an implementation to the abstraction. This makes it difficult to modify, extend, and reuse abstractions and implementations independently.

Let us look at an example:

Let us say we have different editor windows say the HomeWindow, the LayoutWindow etc. And our application supports different themes like DarkTheme, and LightTheme. We have to support each of the editor windows for each theme. When we use inheritance it will look like,

InheritanceExplosion

What happens when we add a new theme?

We have to create a subclass for each of the supported editor windows. Say, we add Dracula theme, then we have to create two new subclasses to support all the existing windows for Dracula (DraculaHomeWindow, and DraculaLayoutWindow).

What happens when we add a new editor?

We have to create a subclass for each of the supported editor themes. If we add a new editor window like CustomEditorWindow we need to add two subclasses for all the supported themes (Dark, Light version of CustomEditorWindow).

Problems with Inheritance

The problem is we are extending in two dimensions i.e., editor window and editor theme.

  • This results in class explosion.
  • It strongly couples the client to both the editor window and the theme i.e., it doesn’t allow us to reuse the abstraction as it is tied to the implementation.

The Bridge Pattern

We thus need to be able to create an editor window without coupling to a theme. The Bridge Design Pattern solves this by putting the Editor Window abstraction and the implementation in separate class hierarchies.

To understand this, I will use a different example.

Let us say we have a racing game where there are multiple landscapes to choose from. The user can also choose from one of the many available vehicles. As we saw earlier, there are two dimensions here; the landscape and the vehicle. So, we will not use inheritance to create one class for each combination of landscape and the vehicle.

By using the Bridge Pattern, we create separate class hierarchies for the abstraction and the implementation.

Abstraction and Implementations

In the Bridge Design Pattern, an abstraction provides operations for the client to operate on. It has a concrete implementation which it uses to perform an operation.

In our example, we create a GameControl object. It has methods to control the game like loading the game screen, and methods for controlling/navigating a vehicle (accelerate, brake etc). We can have more specific GameControl subclasses that vary in the way they implement an operation or has additional operations. The GameControl forms the abstraction part of the hierarchy.

The second hierarchy is for the Vehicle. A Vehicle is an interface that lists the various functionalities of a vehicle. We have subclasses of this (like Car, Jeep, Bus etc). This hierarchy forms the implementation part of the Bridge pattern.

Note that a GameControl is not coupled to a concrete Vehicle. A GameControl (or a more specialized GameControl) has only a reference to the Vehicle interface (contract). The client will pass the actual concrete Vehicle implementation to the abstraction.

This decouples the abstraction from the various implementation. We refer to this relationship between the GameControl and the Vehicle as a bridge, because it bridges the abstraction and its implementation, letting them vary independently.

Bridge implementation

Here is the GameControl class and the Vehicle interface described above.

public class GameControl {
    protected final Vehicle vehicle; //Bridge to an implementation

    public GameControl(Vehicle vehicle) {
        this.vehicle = vehicle;
    }

    public void loadGameScreen() {
        System.out.println("Creating the default game landscape");
        vehicle.start();
    }

    public void accelerate() {
        vehicle.accelerate();
    }

    public void brake() {
        vehicle.brake();
    }

    public void crash() {
        System.out.println("Oops. The vehicle has crashed");
        vehicle.stop();
    }

    public void endGame() {
        System.out.println("The game has ended");
        vehicle.stop();
    }
}
public interface Vehicle {
    void start();

    void stop();

    void accelerate();

    void brake();
}

The loadGameScreen methods loads the game environment, settings, and starts the vehicle. The actual vehicle implementation is not known at compile time. Other methods are self-explanatory. The accelerate and brake game control methods call in to the Vehicle’s method. If the vehicle crashes, we call the crash method of the Vehicle. Once the game is finished, it stops the vehicle.

Here is one possible implementation of a Vehicle - the Car

public class Car implements Vehicle {
    private int speed = 0;
    private int maxSpeed = 10;

    @Override
    public void start() {
        System.out.println("Starting the car");
    }

    @Override
    public void stop() {
        System.out.println("The car is stopped");
        speed = 0;
    }

    @Override
    public void accelerate() {
        speed = Math.max(speed + 1, maxSpeed);
        System.out.println("Accelerating the car");
    }

    @Override
    public void brake() {
        speed = Math.min(speed - 1, 0);
        System.out.println("Applying brakes for the car");
    }
}

Similarly, we can have other Vehicle subclasses like Jeep, Bus etc.

More specific abstractions

The real benefit of the Bridge Design Pattern comes when we have more specific versions of the abstraction. Example: We have a more specific landscapes for Country Side and Modern City.

public class CountrySideLandscapeGameControl extends GameControl {

    public CountrySideLandscapeGameControl(Vehicle vehicle) {
        super(vehicle);
    }

    @Override
    public void loadGameScreen() {
        System.out.println("Loading the country side specific landscape");
    }
}

This loads the screen in a different way.

public class AdvancedCityLandscapeGameControl extends GameControl {
    public AdvancedCityLandscapeGameControl(Vehicle vehicle) {
        super(vehicle);
    }

    @Override
    public void loadGameScreen() {
        System.out.println("Loading the advanced high tech city specific landscape");
    }

    public void applyTurbo() {
        System.out.println("Apply turbo speed");
        for (int i = 0; i < 5; i++) {
            this.vehicle.accelerate();
        }
    }
}

This loads the screen to suit a modern city. It also has an additional method called applyTurbo. Its implementation accelerates the vehicle 5 times.

Summary of the Bridge implementation

  1. The Abstraction provides high-level operations.
  2. There can be more specific (sub-classes) of the abstraction which has more functionalities.
  3. An abstraction depends on an implementation to do the actual work.
  4. The Implementation declares the common methods for all the implementations.
  5. Note that the methods declared on the abstraction and implementation may not be the same. An Abstraction achieves a functionality by using the features provided by implementation.
  6. The client relies on/depends on the abstraction. Hence, the client will pass a concrete implementation to an abstraction.

Structure

BridgeDesignPatternClassDiagram

Participants

Abstraction (GameControl)

  • It defines the abstraction’s interface.
  • Has a reference to an implementation object.

RefinedAbstraction (AdvancedCityLandscapeGameControl)

  • Extends the Abstraction and adds more methods/operations.

Implementor (Vehicle)

  • Interface for the Implementation class.
  • This provides primitive operations that are used by the Abstraction to implement high-level operations.

ConcreteImplementor (Car, Jeep)

  • Implements the Implementor interface.

Advantages of the Bridge Pattern

  • An implementation is not bound to an interface. The implementation of an abstraction can be configured or even changed at run-time.
  • Since we do not couple abstraction and implementation, we avoid proliferation of classes.
  • We can extend both hierarchies independently. This adheres to the Open/Closed Principle.
  • The client is not coupled to implementation and it depends only on the abstraction.
  1. An Adapter Design Pattern is used to make unrelated classes work together i.e., it adapts an interface to another. But a Bridge is used up-front in a design to have two class hierarchies for abstractions and implementations.
  2. An Abstract Factory can create and configure a particular Bridge.
  3. There is some similarity to a Builder Pattern. The director in a builder is like the abstraction in the Bridge; and the builders act as implementations.

Conclusion

We saw the problems in conventional inheritance when we wanted to extend in two dimensions. We wanted to avoid binding or coupling between an abstraction and its implementation. The Bridge design pattern provides a way to decouple the abstractions and implementations by having separate class hierarchies for them. Thus, it lets us combine the different abstractions and implementations and extend them independently. The reference to the implementation in the abstraction is called as the ‘bridge’.

References