JS Pro Design Patterns - The Bridge Pattern

JS Pro Design Patterns - The Bridge Pattern

I have to confess that the way that this pattern was introduced in the book seemed a bit strange for me at the first sight. Especially because it is presented in three different contexts: as a wrapper to decouple function logic, as an encapsulation helper, and as an entity built for enabling the classes it holds to vary independently (the most known and common case for it).

๐Ÿ“œ Implementation

The first use case lies in a very common and practical situation, where a bridge can be applied as an event listener callback. Take a look at the following example, where we simply define a function to be used as a callback of a given click event that performs some logic to fetch data from an API.

function getUserById(e){
    const selectedId = this.id;
    fetch(`/user/${selectedId}`)
        .then(response => response .json())
        .then(data => console.log('Fetched data: ', data))
}

document.addEventListener('click', getUserById);

This example might seem very familiar, right?

That's nothing wrong with it, but it has some downsides like coupling the implementation logic to the callback method, making it hard to unit test. Usually, we will end up using this same fetching logic in other parts of our code so it would be better to find a way to decouple our fetch implementation directly to the event listener callback.

Here is where the bridge comes to play, it will be nothing more than a "wrapper" function which role will be serving as a connection between the callback and the fetching logic.

function getUserById(selectedId){
    return fetch(`/user/${selectedId}`)
        .then(response => response .json())
        .then(data => console.log('Fetched data: ', data))
}

function getBridgedUserById(e){
    const selectedId = this.id;
    getUserById(selectedId)
        .then(_ => console.log('Bridged method!'));
}

document.addEventListener('click', getBridgedUserById);

Although this modification might be silly, it brings us a bunch of benefits and good practices like code reusability, decoupling, and facilitates unit testing.

The second case is pretty straightforward, it relies on data encapsulation, acting as a way to expose data (in this case, acting as an object instance method, a.k.a privileged function).

function Person(name, surname){
    this.name = name;
    this.surname = surname;
    this.getFullName(){
        return `${this.name} ${this.surname}`;
    }
}

The third use of this design pattern is pretty famous in oop, where an entity is created to enable its subclasses to vary independently of each other. As this description may sound a bit abstract, we'll jump to a practical example to facilitate understanding.

Just to add some context to this, imagine we want to model a problem involving smartphones and Android versions. Initially, we were given Samsung and Asus as smartphone brands and Android Marshmallow and Nougat. Translating it to code, it would become something like this:


function SamsungMarshmallowSmartphone(){
    this.name = 'Samsung';
    this.androidVersion = 'Marshmallow';
}

function SamsungNougatSmartphone(){
    this.name = 'Samsung';
    this.androidVersion = 'Nougat';
}

function AsusMarshmallowSmartphone(){
    this.name = 'Asus';
    this.androidVersion = 'Marshmallow';
}

function AsusNougatSmartphone(){
    this.name = 'Asus';
    this.androidVersion = 'Nougat';
}

// To create an specific smartphone we would simply instance it like the following
const samsungMarshmallow = new SamsungMarshmallowSmartphone();
const asusNougat = new AsusNougatSmartphone();

Simple and straightforward, right?

But... Let's imagine that Android Oreo was just launched and we want to include it in our model. We will need to create two more functions, one for the Asus brand and another for Samsung.

Cool! This still won't be a big problem.

Now imagine that we want to improve our modeling with smartphone model types besides the brand. From this moment and on, I suppose you would agree with me that things will get really boring and cumbersome to be added as our model detailing increases.

One solution for that, brought up by the bridge pattern, is to create an entity that will have one attribute holding the brand and another holding the Android version.


function SmartPhone(brand, androidVersion){
    this.brand= brand;
    this.androidVersion = androidVersion;
}

function SamsungPhone(){
    this.name = 'Samsung';
}

function AsusPhone(){
    this.name = 'Asus';
}

function AndroidMarshmallow(){
    this.version = 'Marshmallow';
}

function AndroidNougat(){
    this.version = 'Nougat';
}

// To instantiate a smartphone:

const samsungNougat = new SmartPhone(new SamsungPhone(), new AndroidNougat())

This kind of design will illustrate exactly what I meant before saying that this pattern allows its classes to vary independently. With this approach, note how the brand and Android version were decoupled and how easy it became to manage variations since for every new brand or version released all we need to do is to create a single entity for it. No more worries with those brand and version combinations every time we have to add a new type in our model as now we have the freedom to vary the brand and the Android version of the smartphone the way we want!

๐Ÿ‘ Known Benefits

I would say that the decoupling of the abstractions from their implementations is the most valuable point here as it allows pieces to be independently managed. It facilitates maintainability and makes bugs easier to be spotted. The bridge's objective is to act as a link between abstractions.

๐Ÿ‘Ž Known Drawbacks

It's true that the disadvantages are very low compared to its benefits, but as anything misused, it also comes with a cost. For each added bridge we will increase the number of function calls, which might affect the performance of the application. They also add extra complexity, especially for debugging purposes due to the addition of nested function calls.

โœ”๏ธ Conclusion

This article introduced the main use cases for the bridge pattern. A smartphone modeling problem was presented in order to contextualize and exemplify the use of the bridge pattern. By the end, the major benefits and drawbacks were also covered.

๐Ÿ“– References

  • Ross Harmes and Dustin Diaz. Pro JavaScript Design Patterns.

If you liked this article or are enjoying my blog until here and want to support my efforts, don't forget to share it with your friends and leave your impression about it! Any comments would be appreciated!

Thanks for the reading and happy coding! ๐Ÿ˜ƒ