DEV Community

Cover image for Bridge Pattern in Java
Matheus Bernardes Spilari
Matheus Bernardes Spilari

Posted on

Bridge Pattern in Java

The Bridge Pattern is a structural design pattern that helps decouple abstraction from implementation, allowing them to evolve independently.

This is particularly useful when dealing with multiple dimensions of variability in our system. Instead of having a complex hierarchy of subclasses, we use composition to separate concerns, making our code more maintainable and scalable.

In this post, we'll explore the Bridge Pattern using an easy-to-understand example with different devices and remote controls. ๐Ÿ“บ๐ŸŽฎ


Why Use the Bridge Pattern?

โœ… Reduces Class Explosion โ€“ Avoids deep inheritance trees when dealing with multiple variations.

โœ… Promotes Composition Over Inheritance โ€“ Favors flexibility by allowing independent changes to abstraction and implementation.

โœ… Improves Maintainability โ€“ Enhances readability and separation of concerns.


Problem Without the Bridge Pattern

Imagine we have different types of devices (like TV and Radio) and different remote controls (like Basic Remote and Advanced Remote).

A bad approach would be using inheritance to create a subclass for each combination, like:

  • BasicTVRemote
  • AdvancedTVRemote
  • BasicRadioRemote
  • AdvancedRadioRemote

As we add more device types or remote types, our class hierarchy grows exponentially! ๐Ÿ“ˆ

Example Without the Bridge Pattern (Using Inheritance)

// Parent class for all remotes
public abstract class Remote {
    public abstract void turnOn();
    public abstract void turnOff();
}

// Concrete class for Basic TV Remote
public class BasicTVRemote extends Remote {
    @Override
    public void turnOn() {
        System.out.println("Turning on the TV.");
    }

    @Override
    public void turnOff() {
        System.out.println("Turning off the TV.");
    }
}

// Concrete class for Advanced TV Remote
public class AdvancedTVRemote extends BasicTVRemote {
    public void mute() {
        System.out.println("Muting the TV.");
    }
}

// Concrete class for Basic Radio Remote
public class BasicRadioRemote extends Remote {
    @Override
    public void turnOn() {
        System.out.println("Turning on the Radio.");
    }

    @Override
    public void turnOff() {
        System.out.println("Turning off the Radio.");
    }
}

// Concrete class for Advanced Radio Remote
public class AdvancedRadioRemote extends BasicRadioRemote {
    public void mute() {
        System.out.println("Muting the Radio.");
    }
}

// Main class
public class WithoutBridgeExample {
    public static void main(String[] args) {
        BasicTVRemote basicTV = new BasicTVRemote();
        AdvancedTVRemote advancedTV = new AdvancedTVRemote();
        BasicRadioRemote basicRadio = new BasicRadioRemote();
        AdvancedRadioRemote advancedRadio = new AdvancedRadioRemote();

        basicTV.turnOn();
        advancedTV.mute();
        basicRadio.turnOff();
        advancedRadio.mute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems with This Approach

๐Ÿšจ Class Explosion โ€“ Every new remote type requires new subclasses.

๐Ÿšจ Hard to Maintain โ€“ Changes in the Remote logic require modifications in multiple classes.

๐Ÿšจ Tight Coupling โ€“ Device behavior is tightly linked to Remote behavior.


Solution: Using the Bridge Pattern

Instead of relying on inheritance, we use composition:

  • We separate Devices (TV, Radio, etc.) from Remotes (Basic, Advanced, etc.).
  • Remotes hold a reference to a Device, allowing independent variations.

Step 1: Create the Device Interface

Each device must implement basic functionalities like turning on/off and changing volume.

public interface Device {
    void turnOn();
    void turnOff();
    void setVolume(int volume);
    boolean isOn();
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement Different Devices (TV and Radio)

public class TV implements Device {
    private boolean on = false;
    private int volume = 50;

    @Override
    public void turnOn() {
        on = true;
        System.out.println("TV is now ON");
    }

    @Override
    public void turnOff() {
        on = false;
        System.out.println("TV is now OFF");
    }

    @Override
    public void setVolume(int volume) {
        this.volume = volume;
        System.out.println("TV volume set to " + volume);
    }

    @Override
    public boolean isOn() {
        return on;
    }
}

public class Radio implements Device {
    private boolean on = false;
    private int volume = 30;

    @Override
    public void turnOn() {
        on = true;
        System.out.println("Radio is now ON");
    }

    @Override
    public void turnOff() {
        on = false;
        System.out.println("Radio is now OFF");
    }

    @Override
    public void setVolume(int volume) {
        this.volume = volume;
        System.out.println("Radio volume set to " + volume);
    }

    @Override
    public boolean isOn() {
        return on;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Remote Control Abstraction

Now, we define an abstract class RemoteControl, which uses composition to interact with a Device.

public class RemoteControl {
    protected Device device;

    public RemoteControl(Device device) {
        this.device = device;
    }

    public void togglePower() {
        if (device.isOn()) {
            device.turnOff();
        } else {
            device.turnOn();
        }
    }

    public void volumeUp() {
        device.setVolume(50);
    }

    public void volumeDown() {
        device.setVolume(10);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Extend the Remote Control with More Functionality

Let's create an Advanced Remote that can also mute the device.

public class AdvancedRemoteControl extends RemoteControl {

    public AdvancedRemoteControl(Device device) {
        super(device);
    }

    public void mute() {
        System.out.println("Muting the device");
        device.setVolume(0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Using the Bridge Pattern in the Main Program

public class BridgePatternExample {
    public static void main(String[] args) {
        Device tv = new TV();
        Device radio = new Radio();

        RemoteControl basicRemote = new RemoteControl(tv);
        AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);

        System.out.println("Using Basic Remote with TV:");
        basicRemote.togglePower();
        basicRemote.volumeUp();

        System.out.println("\nUsing Advanced Remote with Radio:");
        advancedRemote.togglePower();
        advancedRemote.mute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Pros of the Bridge Pattern

โœ… Decouples abstraction from implementation โ€“ Devices and Remotes can evolve separately.

โœ… Improves Maintainability โ€“ Changes in one part of the code donโ€™t affect the other.

โœ… Promotes Code Reusability โ€“ You can mix and match different implementations.


Conclusion

The Bridge Pattern is a great solution when you have multiple variations of both abstractions and implementations. By using composition over inheritance, we gain flexibility, scalability, and better code organization.


๐Ÿ“ Reference

๐Ÿ’ป Project Repository

๐Ÿ‘‹ Talk to me

Top comments (0)