loading...

The Bridge Pattern - Design Patterns meet the Frontend

coly010 profile image Colum Ferry ・5 min read

The Bridge Design pattern is one of the design patterns I've had most difficulty wrapping my head around. 🤯 Note: This article assumes some basic knowledge of Interfaces in Object Oriented Programming

In this article, I'll explain what this pattern is, how it can be used and an example of where it is currently used in the Frontend space (psst Angular 🚀).

We'll go over a few things:

  • What is it? 🤔
  • Let's break it down 😱
  • But why? 😐
  • Where can I find it in action? 🚀
  • Where else could I use it? 🎉

What is it? 🤔

The Bridge Pattern can be thought of as part if the Composition over Inheritance Argument. It essentially "bridges a gap" between the Abstraction and the Implementation. These terms might cause a little confusion so lets clear them up.

  • Implementation - This is an interface that will describe a set of specific behaviour that can be used by any Object in our codebase. It can have multiple concrete impletations that adhere to the contract defined in the interface.
  • Abstraction - This is the object that will provide an API that will make use of an underlying Implementation. It acts as a layer over the top of the Implementation that can be further refined through inheritance if required.

Ok, mind blown. 🤯 I know, that can be a bit hairy to read.

So let's take a quick look at a UML Diagram (I know, sigh, but it really does help):

Bridge Design Pattern UML

As we can see from the diagram, the pattern allows us to separate two interfaces that can define the specifics of an object, in this case the type of Shape the object is and the Color that Shape is.

We can create multiple colors, or multiple shapes, without worrying about one impacting the other, therefore increasing loose coupling in our codebase! 🔥

Let's break it down 😱

Keeping our example above in mind, the Abstraction would be our Shape class, whilst our Implementation is the Color class.

The Abstraction contains a reference to the Implementation via a property on the class (hence the composition over inheritance), in our case, Shape has a Color property. Any class that implements the Color contract can then be used by any Shape property.

The consumer of the Abstraction doesn't need to worry about the underlying implementation whilst the pattern itself also increases loose coupling between the Abstraction and the Implementation.

If you're like me, looking at code can help clear things up. So let's do just that!

We'll be using TypeScript for these examples

// Define an interface for the Implementation
interface Color {
    log(): string;
}

// Define an abstract class for the Abstraction
abstract class Shape {
    color: Color;

    constructor(color: Color) {
        this.color = color;
    }

    logMe() {
        console.log(`I am a ${this.color.log()} shape.`);
    }
}

// Create a Concrete Implementation
class Red implements Color {
    log() {
        return 'red';
    }
}

class Blue implements Color {
    log() {
        return 'blue';
    }
}

// Create a refined Abstraction that behaves slightly differently
class Circle extends Shape {
    constructor(color: Color) {
        super(color);
    }

    logMe() {
        console.log(`I am a ${this.color.log()} circle.`);
    }
}

class Triangle extends Shape {
    constructor(color: Color) {
        super(color);
    }
}

// Instantiate the circle with a concrete implementation
const circle = new Circle(new Red());
const triangle = new Triangle(new Blue());

circle.logMe();
// Output: I am a red circle.

triangle.logMe();
// Output: I am a blue shape.

Awesome! We can create as many Colors as we like, or as many Shapes as we like, without affecting one or the other! 🚀🚀🚀

Keeping things simple and separated increases the maintainability and testability of our code, which leads to better code! We can extend and new shapes and colors in the future very easily now too!

But why? 😐

Let's take a look at some reasons why we would use this pattern:

  • The Bridge Pattern decouples the Abstraction and the Implementation, therefore allowing the two to differ independently.
  • It keeps the Abstraction and the Implementation in it's their own inheritance hierarchy, allowing them to grow without affecting the other.
  • The Abstraction does not need to know about the Concrete Implementation and therefore it can be set or swapped at run-time without breaking the Abstraction.

Awesome, but where can I use it? 🤔

Where can I find it in action? 🚀

Ok, the Bridge Pattern is awesome. It can increase our loose coupling, but, where can we actually use it? Where is it being used in the wild?

Angular uses it! (Big thanks to Wes Copeland - @wescopeland_ for pointing this out to me.)

They use it in their Forms API to bridge a gap between any NgControl and any ControlValueAccessor.

The ControlValueAccessor is an interface with methods that must be implemented for any class that implements it. Angular provides it's own concreate implementations of the ControlValueAccessor Implementation, but any developer can implement the interface and any NgControl will be able to use it!

In other words, an implementation outside of the Angular Framework is perfectly acceptable by an Abstraction within the Framework! 🔥🔥

Likewise, a developer can create their own NgControl and any of the concrete implementations provided by Angular will be able to work with it! 💥💥

Hopefully that can help you understand the power behind The Bridge Pattern, but if you still need your own use case, keep reading!

Where else could I use it? 🚀

Well, I've found that a perfect example of this in the Frontend world would be in a Data Access Layer.

You can have the following:

  • An Abstraction defining an Entity Service that will handle logic related to the entities within your system.
  • An Implementation defining an API Interface that allows you to interact with any potential backend system or API.

Let's take a quick look at this in action:

We'll start with our Implementation (API Interface):

export interface IApiService {
    get<T>(): T;
    getAll<T>(): T[];
    add<T>(entity: T): void;
    update<T>(entity: T): void;
    delete<T>(entity: T): void;
}

and next we will define our Abstraction (Entity Service):

export abstract class EntityService {
    apiService: IApiService;

    constructor(apiService: IApiService) {
        this.apiService = apiService;
    }
}

Ok, so we've set up our Abstraction and our Implementation. Let's put them to use!

First let's create a UserService that refines our Abstraction.

export interface User {
    id: string;
    name: string;
    email: string;
}

export class UserService extends EntityService {
    activeUser: User;

    constructor(apiService: IApiService) {
        super(apiService);
    }

    addNewUser(user: User) {
        this.apiService.add(user);
    }

    // Here we perform some logic custom to the UserService
    // But use the underlying bridge between the concrete
    // implementation and the abstraction to update the
    // active user after some custom logic
    setActiveUserEmail(email: string) {
        this.activeUser.email = email;
        this.apiService.update(this.activeUser);
    }
}

Now that we have a Refined Abstraction, let's go ahead and create a Concrete Implementation

export class CustomApiService implements IApiService {
    get<T>(): T {
        // fetch the user from the api
    }

    getAll<T>(): T[] {
        // etc
    }

    add<T>(entity: T): void {
        // etc
    }

    update<T>(entity: T): void {
        // etc
    }

    delete<T>(entity: T): void {
        // etc
    }
}

Ok, now let's use our Concrete Implementation along with our Refined Abstraction:

const apiService = new CustomApiService();
const userService = new UserService(apiService);

userService.addNewUser({...} as User);

Awesome! Our UserService does not need to know the nitty gritties of the IApiService Implementation for it to still perform as expected.

What happens if down the road requirments change and suddenly we can't use the CustomApiService any more! 😱

Have no fear, the Bridge Pattern is here! 😍

Simply create a new Concrete Implementation, and supply that to the UserService instead:

// Create new implementation
export class MongoDBService implements IApiService {
   // etc
}

// Swap out the api service
const apiService = new MongoDbService();

// No need to change the rest!
const userService = new UserService(apiService);

userService.addNewUser({...} as User);

Isn't that awesome! 🚀🚀🚀


Hopefully you learned a bit (more?) about the Bridge Pattern from this article, a potential use-case for it and how it is used in Angular.

If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.

Discussion

pic
Editor guide
Collapse
jwp profile image
John Peters

I feel that all abstraction, as shown in this example are just contracts of what is to be contained by concrete implementations. Whether or not that abstraction includes other interfaces is up to the designer; however, a more overarching principal is that "favoring composition over inheritance" simply means, add anything you need as a property or field/function (typed or non-typed), in this case, it's a Color property type defined by a Color contract/interface.

In the context of Composition, we don't call this a pattern, as it is simply containment of what is needed e.g. Color and Shape (and the rules to follow). Note that the exact same thing can be achieved merely defining classes, no need for interface or abstract classes. In fact, ultimately there is no need for types either.

Most JavaScript folks will not see the value in this example. They don't see value in interfaces and do not use abstract declarations. Shoot they haven't traditionally even used the class construct. For them it's just close-to-the-metal scripting/get-it-done, simple key value pair programming all contained in a JSON Object. In some ways they are right, it's much easier to think that way than in "abstract" OOP terms.

Alas the style we choose for the right job is always paramount, need types? Implement them! Don't need types? Leave them out. Need abstractions? Put them in etc. etc.

Thanks for the post.

Collapse
coly010 profile image
Colum Ferry Author

Thank you for the constructive feedback!
I feel like I found this pattern difficult to wrap my head around was because it is very close to the Strategy Pattern, and it really is just down to fulfilling contracts that the developer defines themselves.

Collapse
jwp profile image
John Peters

I studied patterns for years. In the end I abandoned them. Reason was that all OOP when refactored as far as possible turns into single responsibility functions anyway. The javascript folks love functional programming, so this aligns there too.

Instead of patterns I now create lots and lots of small SRP functions that are compose-able. Also, I no longer use classical inheritance. I find doing this aligns nicely with web based programming. I still use Typescript because I love type safety and I use Classes for the same reason. Why find run-time errors when you can find them via intellisense before the compile?

Collapse
oliverradini profile image
OliverRadini

Maybe I'm missing something, but it seems that the distinction between an abstraction and implementation only becomes relevant when the language has a distinction between the two?

In some senses, Javascript developers work only in abstractions; if I have two different API services in Javascript, and they both implement a .get, how would we distinguish, in Javascript, between writing a function which relies on an implementation of a thing-which-needs-a-get-function, and an abstraction of a thing-which-needs-a-get-function? With an example:

const callGet = apiService => url => apiService.get(url)

const apiA = {
  get: url => console.log(`called get with url ${url} on apiA`)
}

const apiB = {
  get: url => console.log(`called get with url ${url} on apiB`)
}

I can call callGet using either apiA or apiB. Unless I'm missing something here, I don't really see, in fact, how non-OO languages have any way at all of caring about the distinction between abstraction/implementation; I'd be very glad to be pointed in the correct direction though!

Collapse
jwp profile image
John Peters

In traditional Javascript there is no guarantee that the service you show, can know ahead of time if both objects (shown above) have implemented the getter pattern. It would only become known at runtime.

This is my #1 reason for advocating class design with types, and why I prefer Typescript over Javascript.

If we use typed classes, then the question is answered before compile time via intellisense. If either of the objects passed in did not implement getters the red squiggly line would show via intellisense while coding.

Therfore questions like 'how do we know' are answered by Intellisense. It's perhaps the greatest tool for programmers ever; and it works best on Typed objects.

Thread Thread
oliverradini profile image
OliverRadini

Indeed, and I'm also a big fan of Typescript; I think the question of catching at runtime vs compile time is a separate question, though.

My main point was that it isn't really possible to assert that Javascript developers don't use abstract declarations. That's all they use.

Collapse
yaldram profile image
Arsalan Yaldram

Sir thank you so much for this great article : -

Here is how I use services using Composition over Inheritance like so

a. under crud.js I have -

 export const fetchAllResources = state => ({
  fetchAll(config = {}) {
    return API.get(`/${state.resource}`, {
      ...config
    });
  }
});

export const fetchResourceByHierarchyLevel = state => ({
  fetchResourceByHierarchyLevel(hierarchyLevel, config = {}) {
    return API.get(`/${state.resource}/${hierarchyLevel}`, {
      ...config
    });
  }
});

export const fetchHistory = state => ({
  fetchHistory(resourceId, updateValues, config = {}) {
    return API.get(`/${state.resource}/history/${resourceId}`, updateValues, {
      ...config
    });
  }
});

b. Here is my API file like so : -

  import {
  fetchResourceByHierarchyLevel,
  hideResource,
  unHideResource,
  addResource
} from "./crud";

function HighlightsCrud(resource) {
  const state = {
    resource
  };

  return Object.assign(
    state,
    fetchResourceByHierarchyLevel(state),
    addResource(state),
    hideResource(state),
    unHideResource(state)
  );
}

export const highlightsAPI = HighlightsCrud(`highlights`);

What I find after reading your post, your approach is more Object Oriented based suited for TypeScript(which is great) and Type-Safe. I will use your approach next time.

Please share some thoughts, Thanks

Collapse
coly010 profile image
Colum Ferry Author

From ES6 onwards you have access to the class keyword, if you don't want to switch to Typescript just yet :)

Collapse
deathshadow60 profile image
Info Comment marked as low quality/non-constructive by the community. View code of conduct
deathshadow60

that can be a bit hairy to read

Because it's BS bingo. I half expected to see the words proactive and paradigm in there.

I'm really not certain what's worse on this one, the nasty case of "big words real people don't use double-talk", or that you appear to be talking about style whilst showing server-side scripting -- aka where style has next to ZERO blasted business being in the first place when working with web technologies. It's called an external stylesheet, USE IT!

Though that you end up talking about train wreck laundry lists of how NOT to use web technologies that is Angular explains a lot... since "separation of presentation from content" seems to mean jack-all to the folks who use it. Hence why most things built with systems like Angular, React, or Vue end up pissing on usability, accessibility, ease of development, ease of use, and ease of maintenance from so on-high you'd think the almighty just got back from a kegger.

I shudder to think what the results of what you're saying here would actually look like on a live page; though I suspect presentation in the markup, presentation in the scripting, and "Semantics? What's that?!?" are the order of the day; Likely mated to "Yet Another" (trademark pending) case of writing two to ten times the code needed to do the job whilst taking a dump on accessibility and then claiming it's all somehow magically "easier".

See Angular's "forms" where they tell you to write 10k of code that generates 20k of static scripting in the markup for what should be a 5k form -- with zero graceful degradation scripting off/blocked client-side and incomplete markup... All resulting in a walking talking WCAG violation.

Do you have a LIVE example of this actually doing something where it isn't garbage? Bet you don't.

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
deathshadow60 profile image
Info Comment marked as low quality/non-constructive by the community. View code of conduct
deathshadow60

... and there it is, the "wah wah is not" that is so painfully common when anyone dares to disagree. The "oh go make your own thing" fallacy mated to the "wah wah, toxic and negative" cop-out. All likely because you cannot actually argue the point with facts. Aka, you don't like the message, so attack the messenger.

Maybe instead of that, you could try presenting facts to debunk my argument?

Or is discussing things in replies "not a thing" you approve of, in which case why the blazes even HAVE comments on a site like this?

Thread Thread
jwp profile image
John Peters

Your retort to the article actually had fallacies in it too you know, but nobody jumped on you.

Collapse
jwp profile image
Info Comment marked as low quality/non-constructive by the community. View code of conduct
John Peters

deathshadow60, did you take your medications today? just wondering.... be nice, everybody has something to say, if you don't like it click on by.