Bridge design pattern is a structural pattern which allows you to split complex large class or multiple related classes into two hierarchies. These two hierarchies are usually abstraction and implementation which are decoupled from each other.
What is Bridge Pattern?
Bridge pattern basically allows you to separate implementation of a class without affecting the client code that uses it. It decouples abstraction from its implementation. It looks similar to adapter pattern but the intent of the pattern is to decouple two separate hierarchies of classes.
Let’s say we have Shape interface which defines simple methods like draw(). We can create multiple subclasses like Circle, Triangle, Square to create concrete shapes. Now, if we want to draw shapes of different colors, we can define a method like color() and we can apply different colors to our shapes. The problem is if we need different color shapes, we will have to define separate subclass for each of those. For example, we might inherit RedCircle, PurpleCircle and BlueCircle from Circle class. As we add more colors, we might need more subclasses. In fact, the subclasses grow exponentially with each shape and color combinations.
The problem is because there are two dimensions in which we are trying to extend our Shape interface. We are extending in terms of color and their shape. The Bridge design pattern solves this by using composition over inheritance. This way we will have Color field in each concrete Shape class which specifies what color to apply and we delegate the task of applying color to this member variable.
Bridge pattern is useful when you want to divide a large monolithic class with several different variants of functionalities into their own hierarchies.
In Java, Driver class behaves like a bridge with DriverManager. java.sql.Driver interface dfeines a set of methods for interacting with databases like connect(String url, Properties props) and database specific implementation like com.mysql.cj.jdbc.Driver and oracle.jdbc.driver.OracleDriver implement these methods. java.sql.DriverManager serves as a mediator between client code and database drivers even though it doesn’t completely encapsulate implementation details.
How to implement Bridge Design Pattern?
In this example, we are going to take example of having Laptop and CellPhone devices with different operating systems like Linux, Windows and iOS. If we try to implement this hierarchy as a inheritance, we can have Device as top level interface and then two subclasses for Laptop and CellPhone. This will further be subclassed into WindowsLaptop, LinuxLaptop and MacLaptop. If we add one more device, we will need to create three new subclasses or if we add one more OS, we will need two subclasses for each device. This is going to get too big very quickly.
To solve this we can create two hierarchy: One for OperatingSystem and another for Device. The Device should have protected reference to OperatingSystem which means each concrete classes have to have OS defined in them for it to work.
- First we have to find out different dimensions which need to be separated into two class hierarchies.
- Create
interfacefor the implementation (OperatingSystem) and create abstract class (Device) for abstraction. This abstract class should includeprotectedreference to the interface we created. - Implement this interface for the type of operating systems we need
Linux,Windows, etc. - Create subclasses for
DevicelikeLaptop,CellPhone. Each of them will need reference toOperatingSystem.
Example: Operating System hierarchy
The OperatingSystem interface defines the basic functionalities of operating system.
1public interface OperatingSystem {
2 void shutdown();
3 void reboot();
4 void installOS();
5 void displayInfo();
6}
The concrete implementation of operating system provide the specific implementation for these behaviors.
1public class Linux implements OperatingSystem {
2 @Override
3 public void shutdown() {
4 System.out.println("Shutting down Linux");
5 }
6
7 @Override
8 public void reboot() {
9 System.out.println("Rebooting Linux");
10 }
11
12 @Override
13 public void installOS() {
14 System.out.println("Installing Linux");
15 }
16
17 @Override
18 public void displayInfo() {
19 System.out.println("Linux OS");
20 }
21
22 @Override
23 public String toString() {
24 return "Linux Operating System";
25 }
26}
1public class Windows implements OperatingSystem {
2 @Override
3 public void shutdown() {
4 System.out.println("Shutting down Windows");
5 }
6
7 @Override
8 public void reboot() {
9 System.out.println("Rebooting Windows");
10 }
11
12 @Override
13 public void installOS() {
14 System.out.println("Installing Windows");
15 }
16
17 @Override
18 public void displayInfo() {
19 System.out.println("Windows OS");
20 }
21
22 @Override
23 public String toString() {
24 return "Windows Operating System";
25 }
26}
Example: Device hierarchy
The Device is an abstract class that holds protected reference to OperatingSystem so that each concrete implementation will have to have OS.
1public abstract class Device {
2 protected OperatingSystem os;
3
4 public Device(OperatingSystem os) {
5 this.os = os;
6 }
7
8 public void shutdown() {
9 os.shutdown();
10 }
11
12 public void reboot() {
13 os.reboot();
14 }
15
16 public void installOS() {
17 os.installOS();
18 }
19
20 public void displayInfo() {
21 os.displayInfo();
22 }
23
24 public abstract void displayDevice();
25
26 public void setOs(OperatingSystem os) {
27 this.os = os;
28 }
29
30 public OperatingSystem getOs() {
31 return os;
32 }
33
34 @Override
35 public String toString() {
36 return "Device with " + os.toString();
37 }
38}
Concrete Device implementation will extend Device and have reference to OperatingSystem. They can also have additional methods.
1public class CellPhone extends Device {
2 public CellPhone(OperatingSystem os) {
3 super(os);
4 }
5
6 @Override
7 public void displayDevice() {
8 System.out.println("Cell Phone");
9 }
10
11 @Override
12 public String toString() {
13 return "Cell Phone " + super.toString();
14 }
15}
1public class Laptop extends Device {
2 public Laptop(OperatingSystem os) {
3 super(os);
4 }
5
6 @Override
7 public void displayDevice() {
8 System.out.println("Laptop");
9 }
10
11 @Override
12 public String toString() {
13 return "Laptop " + super.toString();
14 }
15}
With these structure, we can independently add new OS or new device. Every time we add new device, we just have to add one subclass of Device and every time we add new OS, we have to only implement one class with OperatingSystem interface. They are decoupled from each other and it is more maintainable.
Now, client code can easily create different kinds of devices and OS combinations. It is client’s responsibility to provide correct implementation for the abstraction object.
1public class DeviceClient {
2 public static void main(String[] args) {
3 Device cellPhone = new CellPhone(new Windows());
4 cellPhone.installOS();
5 cellPhone.reboot();
6 cellPhone.displayDevice();
7 cellPhone.shutdown();
8
9 System.out.println("=====================================");
10
11 Device laptop = new Laptop(new Linux());
12 System.out.println(laptop);
13 }
14}
Advantages:
- This pattern improves decoupling by separating the abstraction from the implementation. Both of those can be developed further independent of each other.
- This improves maintainability and extensibility as new implementations can be added easily without modifying abstraction.
- This improves code reusability as we do not have to recreate methods defined in implementations. We can simply delegate the work to those from abstraction layer.
Disadvantages:
- It may be difficult to plan upfront and can be somewhat confusing at first to decide what goes where.
Comparison with Adapter Pattern
| Adapter Pattern | Bridge Pattern |
|---|---|
| useful for legacy code or third party code | useful for upfront design decisions |
| This is retrofitten in existing code | This is built in advance |
| provides different interface for non-compatible legacy code | provides same abstraction |
| simpler than bridge | little more complex to implement |
Summary
- The bridge pattern can be used to separate two separate dimensions into two hierarchies.
- This pattern provides lot more flexiblity for code reuse and maintenance.
- It uses composition over inheritance.


Comments