Skip to main content

Understanding The Bridge Design Pattern

The Bridge pattern is a structural design pattern that lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other.

The Bridge pattern is called so because it acts as a bridge between the abstraction and its implementation. This pattern decouples the abstraction from the implementation so they can vary independently. This decoupling enables you to modify your codebase with much less effort and makes your code more flexible.

In this diagram, MediaPlayerImplementation is an interface which is implemented by two classes: WindowsMediaPlayer and MacOSMediaPlayer. These classes implement the playAudio and playVideo methods.

The MediaPlayerAbstraction class has a property implementation which is of type MediaPlayerImplementation. It has an abstract method playFile.

There are two classes AudioPlayer and VideoPlayer that extend the MediaPlayerAbstraction class and implement the playFile method.

Implementation Of The Bridge Pattern

  • Abstraction defines the interface that the clients interact with and contains a reference to the implementation.
  • Implementation defines an interface for all implementation classes.
  • Concrete Implementations are distinct classes implementing the implementation interface.
  • Refined Abstractions are variants of the Abstraction for specific tasks.

Now let's move on to a TypeScript example. Let's consider a scenario where we are creating a media player that can play different types of media files.

1. Implementation interface and concrete implementations:

// The implementation interface defines methods for
// all Concrete Implementation classes.
interface MediaPlayerImplementation {
playAudio(): void;

playVideo(): void;
}

// Each Concrete Implementation corresponds to a
// specific platform and implements the (platform-)specific code.
class WindowsMediaPlayer implements MediaPlayerImplementation {
playAudio(): void {
console.log("Playing audio on Windows media player.");
}

playVideo(): void {
console.log("Playing video on Windows media player.");
}
}

class MacOSMediaPlayer implements MediaPlayerImplementation {
playAudio(): void {
console.log("Playing audio on MacOS media player.");
}

playVideo(): void {
console.log("Playing video on MacOS media player.");
}
}

2. Abstraction and refined abstractions:

// The Abstraction provides high-level control logic.
// It relies on the implementation object to do the actual low-level work.
abstract class MediaPlayerAbstraction {
protected implementation: MediaPlayerImplementation;

constructor(implementation: MediaPlayerImplementation) {
this.implementation = implementation;
}

abstract playFile(): void;
}

// You can extend the Abstraction without changing the Implementation classes.
class AudioPlayer extends MediaPlayerAbstraction {
playFile(): void {
this.implementation.playAudio();
}
}

class VideoPlayer extends MediaPlayerAbstraction {
playFile(): void {
this.implementation.playVideo();
}
}

3. Client code:

// The client code should only depend on the Abstraction class.
// This way the client code can support any abstraction-implementation combination.
let player = new AudioPlayer(new WindowsMediaPlayer());
player.playFile(); // 'Playing audio on Windows media player.'

player = new VideoPlayer(new MacOSMediaPlayer());
player.playFile(); // 'Playing video on MacOS media player.'

This pattern is especially useful when dealing with cross-platform applications or different database systems where you want to keep your business logic independent from the underlying system's nuances.

When To Use Bridge Pattern

There are several scenarios where you might want to decouple an abstraction from its implementation:

  1. When you want to hide implementation details from the client: By splitting the interface (or the abstraction) from the implementation, you can expose only the methods that the client needs to see. This separation encapsulates the complexities and makes your code easier to read and maintain.

  2. When you want to have implementation-specific behavior: If you have a class that has platform-specific code, decoupling the abstraction from its implementation allows you to create different implementations for each platform and easily switch between them without changing the client code.

  3. When you want to switch implementations at runtime: By decoupling an abstraction from its implementation, it's possible to switch implementations at runtime. This flexibility is useful in scenarios where your program needs to behave differently depending on the environment it's running in or other runtime conditions.

  4. When your code structure is static, but the behavior needs to be dynamic: The Bridge pattern allows you to create a stable structure of classes while providing dynamic behavior. The structure of your code doesn't change even as the behavior of the application can change based on the specific implementation that is used.

  5. When you want to prevent monolithic designs: The Bridge pattern promotes modularity in your design. It helps to prevent "monolithic" class hierarchies where one change could have widespread implications.

By decoupling abstractions and implementations, the Bridge pattern makes your code more flexible and adaptable to changes. It can be a powerful tool for handling situations where you expect the implementation details or the higher-level abstractions of your code to change frequently or independently over time.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.

YouTube @cloudaffle