DEV Community

Cover image for The Bridge Design Pattern in JavaScript
jsmanifest
jsmanifest

Posted on • Originally published at jsmanifest.com on

The Bridge Design Pattern in JavaScript

Image description

In this article we will be going over the Bridge Design Pattern in JavaScript. This is one of the top used patterns that make a significant impact in softare applications. It is a pattern that easily promotes a separation of concerns in its implementation and it's scalable.

Here is diagram depicting this pattern:

javascript-bridge-design-pattern-diagram

There are usually two main participants (or entity, whichever you want to call it) that are involved in the Bridge Pattern.

The first and top most part is the abstract layer. This can be implemented simply as a class:

class Person {
  constructor(name) {
    this.name = name
  }

  talk(message) {
    console.log(message)
  }
}
Enter fullscreen mode Exit fullscreen mode

In the Bridge Pattern, the abstract layer declares the base interface methods and/or properties. However, they do not care about the implementation details because that isn't their job. To be able to reap the advantages of this pattern it must be kept this way so that our code later does not become tightly coupled and remains manageable.

The abstract layer instead opens bridges which then leads to the second main part of the pattern: the implementation layers (which are often implemented as classes in practice) are attached to these bridges, which the client (or you) call the shots. The word "attached" is my form of a human readable term to understand the code term which are references or pointers:

bridge-pattern-real-life-vs-code-diagram1.png

The "bridge" can visibly appear in code like this:

class Theme {
  constructor(colorScheme) {
    this.colorScheme = colorScheme // Bridge declared
  }

  getColorScheme() {
    return this.colorScheme // Bridge reference/pointer
  }
}
Enter fullscreen mode Exit fullscreen mode

If you've visited websites like https://dev.to or https://medium.com they have a theme feature that you can access inside your profile. There is usually a toggle theme button. The theme is the abstract layer. The actual implementation in toggling between light and dark are most likely located outside of the abstract layer location within the implementation layer(s).

Where and when should the Bridge Pattern be used?

Some implementations in the real world are coded in a way where the "bridge effect" goes "live" during run time. When you need this type of coupling / binding between two objects this is when you can use the Bridge Pattern to your advantage.

A good example of this is twilio-video, a JavaScript library that lets you add real time voice and video to your web applications (like Zoom). In this library, The Room always instantiates as an empty room. The class keeps a pointer to a LocalParticipant, (when you join a video chat room you are the LocalParticipant on your screen) but the LocalParticipant doesn't actually run or become instantiated yet until it connects and is finished subscribing to the room which is only possible in running code.

If you scan through their code you will spot bridges in a lot of areas. A video chat session cannot be created without a Room, and a room does not start until there are at least two Participants. But a Participant cannot begin streaming until they start their local audio/video MediaTracks. These classes work together in a top down hierarchy. When you start to have multiple classes that are coupled together this is also a good time to consider the Bridge Pattern.

Another scenario where the Bridge Pattern is useful is when you want to share an implementation of some object with multiple objects.

For example, the MediaStreamTrack class represents a media track for a stream. The two most common implementations that "bridge" from it are audio and video tracks.

bridge-design-pattern-media-stream-track.png

In addition, the implementation details are usually hidden within the derived classes.

Implementation

Let's implement our own variation of the Bridge Pattern to get a good feel of a problem and solution it brings to the table.

Let's start with a generic Thing class which can represent any thing:

class Thing {
  constructor(name, thing) {
    this.name = name
    this.thing = thing
  }
}
Enter fullscreen mode Exit fullscreen mode

We can create a high level abstraction class that extends Thing. We can call this LivingThing and will define a method called eat. All living things in the real world are born with the ability to eat in order to stay alive. We can mimic this in our code. This will stay in the high level abstract layer:

class LivingThing extends Thing {
  constructor(name, bodyParts) {
    super(name, this)
    this.name = name
    // Bridge
    this.mouth = bodyParts?.mouth || null
  }

  eat(food) {
    this.mouth.open()
    this.mouth.chew(food)
    this.mouth.swallow()
    return this
  }
}
Enter fullscreen mode Exit fullscreen mode

We can see that we opened a bridge to the Mouth class. Let's define that class next:

class Mouth extends Thing {
  constructor() {
    super('mouth', this)
  }

  chew() {}
  open() {}
  swallow() {}
}
Enter fullscreen mode Exit fullscreen mode

The thing (no pun intended) to consider now is that our Mouth will be an implementation layer where we write the logic for communicating between the mouth and food.

This implementation is entirely based in Mouth. The LivingThing does not care about these implementation details and instead delegates this role entirely to its implementation classes which in our case is Mouth.

Let's pause and talk about this part for a moment. If LivingThing is not involved in any of its implementations this is actually a useful concept to us. If we can make other LivingThings that only need to provide the interface for implementations to derive from, then we can make a wider range of classes for other scenarios.

In an MMORPG game we can use the LivingThing and make more of them where they all inherit a pointer to a mouth automatically:

class Character extends LivingThing {
  constructor(name, thing) {
    super(name, this)
    this.thing = thing
    this.hp = 100
    this.chewing = null
  }

  attack(target) {
    target.hp -= 5
    return this
  }

  chew(food) {
    this.chewing = food
    return this
  }

  eat(food) {
    this.hp += this.chewing.hpCount
    return this
  }
}

class Swordsman extends Character {}
class Rogue extends Character {}
class Archer extends Character {}
class Sorceress extends Character {}

class Potion {
  constructor(potion) {
    this.potion = potion
  }

  consume(target) {
    if (this.potion) {
      this.eat(this.potion)
      this.potion = null
    }
  }
}

class Food {...}

const sally = new Sorceress()
const mike = new Rogue()

mike.attack(sally)
sally.eat(new Food(...))
Enter fullscreen mode Exit fullscreen mode

The bridge pattern is well known to enable developers to build cross-platform applications. We can already see this capability in our examples. We can build this same MMORPG game by reusing LivingThing on a new code base. We only need to re-implement the implementation layers like Mouth in order to create bindings to different platforms.

We aren't limited to games. Since our LivingThing is generic and makes sense for anything that moves it's possible we can use it to create something entirely different like a robot as an IoT device program and simulate eating behavior with LivingThing.

Going back into our pretend MMORPG game, bridges can be used to create more bridges. MMORPG usually have some profile page where users can edit their settings.

This Profile can itself utilize the Bridge Design Pattern to define a suite of pieces to make it function like a profile api:

let key = 0

class Profile {
  constructor({ avatar, character, gender, username }) {
    this.character = null // Bridge
    this.gender = null
    this.username = username
    this.id = ++key
  }

  setCharacter(value) {
    this.character = value
    return this
  }

  setGender(value) {
    this.gender = value
    if (value === 'female') {
      this.showRecommendedEquipments('female')
    } else {
      this.showRecommendedEquipments('male')
    }
    return this
  }

  setUsername(value) {
    this.username = value
    return this
  }

  showRecommendedEquipments() {
    // Do something with this.character
  }

  save() {
    return fetch(`https://some-database-endpoint.com/v1/profile/${key}`, {
      method: 'POST',
      body: JSON.stringify({
        character: this.character,
        gender: this.gender,
        username: this.username,
      }),
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

If you've read some of my other articles this might feel similar to the Adapter or Strategy pattern.

There are distinct differences that solve different problems however:

In the Adapter pattern the problem it solves starts from the code (or prior to runtime) where we would construct the Adapter first then immediately start with the rest:

axios-mock-adapter

function adapter() {
  return function (config) {
    var mockAdapter = this
    // axios >= 0.13.0 only passes the config and expects a promise to be
    // returned. axios < 0.13.0 passes (config, resolve, reject).
    if (arguments.length === 3) {
      handleRequest(mockAdapter, arguments[0], arguments[1], arguments[2])
    } else {
      return new Promise(function (resolve, reject) {
        handleRequest(mockAdapter, resolve, reject, config)
      })
    }
  }.bind(this)
}
Enter fullscreen mode Exit fullscreen mode

Compare that with our earlier snippets of twilio-video and you will feel the difference immediately.

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!

Discussion (0)