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 (3)

Collapse
 
try_code profile image
sa sa

this a great article to address design pattern, i have this case and confuse how to use which pattern to solve it
case:
"Imagine there is a corridor where this corridor processes requests, and these requests have different characteristics. However, the corridor is singular, so all requests must pass through it to be processed into the desired outcomes. Some requests use the same processes within the corridor, but the order of those processes varies."

In your opinion, what design pattern should I implement for the corridor and the requests so that my application adheres to the SOLID principles?

Collapse
 
mspilari profile image
Matheus Bernardes Spilari

Hi @try_code, glad that you enjoy the article.
Ok, I think that a good approach for this scenario would be to combine the Chain of Responsibility and Strategy design patterns.
The Chain of Responsibility allows you to model the "corridor" as a chain of processing steps, where each step can handle (or not) the request and then pass it along to the next.
The Strategy pattern can be used to define different strategies for ordering and combining these steps, since each request may require a different sequence of processes.
This combination keeps the system flexible and modular, making it easier to maintain and extend, while also adhering well to the SOLID principles.

I have an article that explains the Strategy pattern here.
Unfortunately, I havenโ€™t written any examples on the blog using the Chain of Responsibility yet, but I highly recommend checking out Refactoring Guru, it's a great resource for learning design patterns.

Hope this helps you solve your problem.
Thanks again for your comment!

Collapse
 
try_code profile image
sa sa

Thank you for your response; I really appreciate it. I will review the pattern you suggested.