Programming with flexibility in mind can be difficult, and sometimes results in writing more classes than were needed. For example, if you wanted to create an app that had multiple UI themes such as light mode and dark mode, it would be unnecessary to create a whole new set of classes for every UI element if the only variation is the theme.

What would be much easier to maintain is a system where the UI theme and UI elements can vary independently; creating a new theme shouldn't require creating each UI element over again. An easy solution to this situation is the bridge pattern!

Assumptions

What is the Bridge Pattern?

The bridge pattern is a structural design pattern that places a layer of abstraction between an abstraction and implementation so that they can vary independently.

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/BridgePattern_01.png" alt="bridge pattern UML diagram." style="max-width:95%;"> </div>

The roles of the bridge pattern are:

  • Abstraction: an abstract class that holds an instance of the implementor.
  • Implementor: an interface that introduces variability in the abstraction.
  • Concrete Abstraction: a class that extends the abstraction interface.
  • Concrete Implementor: a class that implements the implementor interface.

In addition to the abstractions and implementations being varied, we can extend implementations to provide further functionality.

Conceptualization

To understand the power that this pattern offers, let's look at the UI example from the intro that doesn't use a bridge pattern:

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/BridgePattern_02.png" alt="bad UI design hierarchy." style="max-width:95%;"> </div>

As we can see, there's a lot of duplicate classes that only vary in name by the theme they implement. This is a code smell that can be remedied with help from the "don't repeat yourself" principle!

If we abstracted our UI and Theme so that the themes can change without needing to tweak the UI, then we end up with a design that looks like this:

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/BridgePattern_03.png" alt="good UI design heirarchy" style="max-width:95%;"> </div>

On top of needing to write less code with this design, tweaking one of the color's only means changing code in a single place instead of every class that implements it. Our bridge pattern solution should look like this diagram:

<div style="width:100%; margin:auto;text-align:center;"><img src="https://www.devmaking.com/img/topics/designpatterns/BridgePattern_04.png" alt="Good UI design UML diagram." style="max-width:95%;"> </div>

Let's implement a solution to our UI example using the bridge pattern. The first thing we'll want to do is define our abstraction, UIObject, and our implementor, Theme.

// An interface that defines the needed colors for a basic theme.
interface Theme {
    /* Uses RGB HEX codes for color values */
   // Backgrounds
   String background();
   String altBackground();
   // Lines and graphs
   String lineColor();
   String altLineColor();
   // Text colors
   String fontColor();
   String altFontColor();
 }
// Defines an abstract class for UI elements
abstract class UIObject {
   // Our view theme object that we want to vary independently.
   Theme viewTheme;
   // Constructor
   UIObject(Theme theme) {
     viewTheme = theme;
   }
   // Draws the UIObject to the screen.
    void draw();
 }

Our Theme interface defines methods that return a particular color value, and is general enough to be used across the application. Our abstraction, UIObject, gets a reference to a Theme implementation through dependency injection.

Having defined the abstractions, let's implement Theme for light mode and dark mode:

// Light Mode implementation
class LightModeTheme implements Theme {
   String background() { return "#ffffff"; }
   String altBackground() { return "#eeeeee"; }
   String lineColor() { return "#333333"; }
   String altLineColor() { return "#8888ff"; }
   String fontColor() { return "#000000"; }
   String altFontColor() { return "#555555"; }
 }

// Dark Mode implementation
class DarkModeTheme implements Theme {
   String background() { return "#222222"; }
   String altBackground() { return "#444444"; }
   String lineColor() { return "#dddddd"; }
   String altLineColor() { return "#dfdfdf"; }
   String fontColor() { return "#eeeeee"; }
   String altFontColor() { return "#aaaaaa"; }
 }

Lastly, let's create a few classes that extend our UIObject abstraction; a button class, UIButton, and a graph chart class UIGraph:

class UIButton extends UIObject {
   // Constructor
   UIButton(Theme theme) {
     super(theme);
   }
   
   void draw() {
        /* Implementation */
     print("Drawing a button on the screen..");
     print("Text color value is: " + theme.fontColor());
     print("Button color value is: " + theme.backgroundColor());
     print("Done drawing the button!");
   }
   // More methods for a button (onClick, etc..);
   
 }

class UIGraph extends UIObject {
   // Constructor
   UIGraph(Theme theme) {
     super(theme);
   }
   
   void draw() {
     /* Implementation */
     print("Drawing a graph on the screen...");
     print("Text color value is " + theme.fontColor());
     print("Axis text color value is " + theme.altFontColor());
     print("Background color value is " + theme.backgroundColor());
     print("Line color value is " + theme.lineColor());
     print("Done drawing the graph!");
   }
   
   // More methods that help the graph (data and such)...
 }

> In a real implementation, we'd use a graphics library to help us actually draw shapes and UI to a window, but we're keeping it simple to explain the concept!

With all our classes defined, we can see what it looks like when applied in code!

static void main(String[] args) {
   // Instantiating our themes:
   Theme lightTheme = new LightModeTheme();
   Theme darkTheme = new DarkModeTheme();
   // Creating a dark button:
   UIButton darkButton = new UIButton(darkTheme);
   darkButton.draw();
   
print("*****");
   // Creating a light graph:
   UIGraph lightGraph = new UIGraph(lightTheme);
   lightGraph.draw();
 }

> Challenge: extend the Theme interface to allow defining a custom theme at runtime!