Bridge Pattern in Java (with Example)

The bridge design pattern is used to decouple a class into two parts – abstraction and its implementation – so that both can evolve in the future without affecting each other. It increases the loose coupling between class’s abstraction and its implementation.

Decouple an abstraction from its implementation so that the two can vary independently.

The bridge is a synonym for the “handle/body” idiom. This is a design mechanism that encapsulates an implementation class inside of an interface class. The former (class) is the body, and the latter (interface) is the handle. The handle is viewed by the user as the actual class, but the work is done in the body.

We get this decoupling by adding one more redirection between method calls from abstraction to implementation.

1. Design Participants

Bridge pattern participants
Bridge pattern participants

Following participants constitute the bridge design pattern.

  • Abstraction (abstract class): It defined the abstract interface i.e. behavior part. It also maintains the Implementer reference.
  • RefinedAbstraction (normal class): It extends the interface defined by Abstraction.
  • Implementer (interface): It defines the interface for implementation classes. This interface does not need to correspond directly to the abstraction interface and can be very different. Implementer provides an implementation in terms of operations provided by the Implementer interface.
  • ConcreteImplementor (normal class): It implements the Implementer interface.

2. When do we need the bridge pattern?

The bridge pattern is an application of the old advice, “prefer composition over inheritance“. It becomes handy when you must subclass different times in ways that are orthogonal with one another.

For example, let’s say you are creating various GUI shapes with different colors. One solution could be:

Without bridge pattern
Without bridge pattern

But the above solution has a problem. If you want to change Rectange class, then you may end up changing BlueRectangle and RedRectangle as well – and even if the change is color specific then you may need to change Circle classes as well.

We can solve the above problem by decoupling the Shape and Color interfaces in the below manner.

With bridge pattern
With bridge pattern

Now when you change any Shape, the color would be unchanged. Similarily, vice-versa.

3. A Design Problem

Bridge design pattern is most applicable in applications where we need to provide the components that are platform independence.

Let’s say, we are designing an application that allows us to down and store files on any operating system.

  • We want to design the system in such a way, we should be able to add more platform support in the future with minimum change.
  • Additionally, If I want to add more support in the downloader class (e.g. delete the download in Windows only), then It should not affect the client code as well as the Linux downloader.

4. Solution using Bridge Pattern

As this problem is a classical platform independence-related problem, I will use a bridge pattern to solve this. I will break the downloader component into abstraction and implementer parts.

Here I am creating two interfaces, FileDownloaderAbstraction represents the abstraction with which the client will interact; and FileDownloadImplementor which represents the implementation. In this way, both hierarchies can evolve separately without affecting each other.

FileDownloaderAbstraction.java

public interface FileDownloaderAbstraction {

    public Object download(String path);
    public boolean store(Object object);
}

FileDownloaderAbstractionImpl.java

public class FileDownloaderAbstractionImpl implements FileDownloaderAbstraction {

    private FileDownloadImplementor provider = null;

    public FileDownloaderAbstractionImpl(FileDownloadImplementor provider) {
        super();
        this.provider = provider;
    }

    @Override
    public Object download(String path)
    {
        return provider.downloadFile(path);
    }

    @Override
    public boolean store(Object object)
    {
        return provider.storeFile(object);
    }
}

FileDownloadImplementor.java

public interface FileDownloadImplementor {

    public Object downloadFile(String path);
    public boolean storeFile(Object object);
}

LinuxFileDownloadImplementor.java

public class LinuxFileDownloadImplementor implements FileDownloadImplementor {

    @Override
    public Object downloadFile(String path) {
        return new Object();
    }

    @Override
    public boolean storeFile(Object object) {
        System.out.println("File downloaded successfully in LINUX !!");
        return true;
    }
}

WindowsFileDownloadImplementor.java

public class WindowsFileDownloadImplementor implements FileDownloadImplementor {

    @Override
    public Object downloadFile(String path) {
        return new Object();
    }

    @Override
    public boolean storeFile(Object object) {
        System.out.println("File downloaded successfully in WINDOWS !!");
        return true;
    }
}

Now let us see how to use the above code:

String os = "linux";
FileDownloaderAbstraction downloader = null;

switch (os)
{
  case "windows":
    downloader = new FileDownloaderAbstractionImpl( new WindowsFileDownloadImplementor() );
    break;

  case "linux":
    downloader = new FileDownloaderAbstractionImpl( new LinuxFileDownloadImplementor() );
    break;

  default:
    System.out.println("OS not supported !!");
}

Object fileContent = downloader.download("some path");
downloader.store(fileContent);

The program output:

File downloaded successfully in LINUX !!

5. Change in abstraction does not affect the implementation

Now let’s say you want to add one more capability (i.e. delete) at the abstraction layer. It must not force a change in existing implementers and clients as well.

FileDownloaderAbstraction.java

public interface FileDownloaderAbstraction {

    public Object download(String path);
    public boolean store(Object object);
    public boolean delete(String object);
}

FileDownloaderAbstractionImpl.java

public class FileDownloaderAbstractionImpl implements FileDownloaderAbstraction {

    private FileDownloadImplementor provider = null;

    public FileDownloaderAbstractionImpl(FileDownloadImplementor provider) {
        super();
        this.provider = provider;
    }

    @Override
    public Object download(String path)
    {
        return provider.downloadFile(path);
    }

    @Override
    public boolean store(Object object)
    {
        return provider.storeFile(object);
    }

    @Override
    public boolean delete(String object) {
        return false;
    }
}

The above change does not force you to make any changes in the implementor’s classes/interface.

6. Change in implementation does not affect abstraction

Let’s say you want to add a delete feature at the implementation layer for all downloaders (an internal feature) which the client should not know about.

FileDownloadImplementor.java

public interface FileDownloadImplementor {

    public Object downloadFile(String path);
    public boolean storeFile(Object object);
    public boolean delete(String object);
}

LinuxFileDownloadImplementor.java

public class LinuxFileDownloadImplementor implements FileDownloadImplementor {

    @Override
    public Object downloadFile(String path) {
        return new Object();
    }

    @Override
    public boolean storeFile(Object object) {
        System.out.println("File downloaded successfully in LINUX !!");
        return true;
    }

    @Override
    public boolean delete(String object) {
        return false;
    }
}

WindowsFileDownloadImplementor.java

public class WindowsFileDownloadImplementor implements FileDownloadImplementor {

    @Override
    public Object downloadFile(String path) {
        return new Object();
    }

    @Override
    public boolean storeFile(Object object) {
        System.out.println("File downloaded successfully in LINUX !!");
        return true;
    }

    @Override
    public boolean delete(String object) {
        return false;
    }
}

The above change does not affect the abstraction layer, so the client will not be impacted at all.

7. Summary

  • Bridge pattern decouple an abstraction from its implementation so that the two can vary independently.
  • It is used mainly for implementing platform independence features.
  • It adds one more method-level redirection to achieve the objective.
  • Publish the abstraction interface in the separate inheritance hierarchy, and put the implementation in its own inheritance hierarchy.
  • Use bridge pattern to run-time binding of the implementation.
  • Use bridge pattern to map orthogonal class hierarchies
  • The bridge is designed up-front to let the abstraction and the implementation vary independently.

Happy Learning !!

Comments

Subscribe
Notify of
guest
4 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.