🙋🏻♀️ What is the Bridge Pattern?
The Bridge Pattern is a software design approach that creates a bridge
between the two separate hierarchies and allows them to have multiple variations and evolve independently, increasing the flexibility of the system.
In a big system, adding a new feature will make the number of class combinations grow exponentially, requiring more classes. Then, the bridge
can be a solution to deal with this problem by allowing classes to be divided into 2 hierarchies called abstraction
and implementation
.
abstraction
: a high-level interface that references animplementation
hierarchy.implementation
: low-level implementation details.
Now, the client can work with the abstraction
without being tightly coupled to a specific implementation
. This pattern favors object composition over class inheritance, following the principle “prefer composition over inheritance.”
✨ Simple TypeScript Example
Let’s say you have to build a design system that has many components and supports different themes. You are also expecting many changes in any theme details as well as components’ specifications. It’s time to utilize the Bridge Pattern!
Here, the Theme
interface serves as the implementation
side of the Bridge pattern. It declares the provideColorSet()
method.
// Implementation
interface Theme {
provideColorSet(): void
}
// Concrete Implementation
class LightTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using light colors...')
}
}
class DarkTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using dark colors...')
}
}
class HighContrastTheme implements Theme {
provideColorSet(): void {
console.log('Provide color set using high contrast colors...')
}
}
The three different theme classes are concrete implementations of the Theme
interface(LightTheme
, DarkTheme
, and HighContrastTheme
.) Each class implements the provideColorSet()
method with specific logic for providing color sets.
The Component
interface represents the abstraction
side of the Bridge pattern. It declares the render()
method, which represents the high-level behavior that relies on the theme.
// Abstraction
interface Component {
render(): void
}
// Refined Abstraction
class Button implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a button.')
}
}
class Dialog implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a dialog.')
}
}
class Form implements Component {
private readonly theme: Theme
constructor(theme: Theme) {
this.theme = theme
}
render(): void {
this.theme.provideColorSet()
console.log('Drawing a form.')
}
}
The Button
, Dialog
, and Form
classes are refined abstractions that implement the Component
interface. Each refined abstraction depends on the Theme
interface, which acts as a bridge to different concrete implementations of the theme. This separation allows for flexible composition and runtime selection of themes without modifying the components themselves.
On the client side, the instance of the concrete implementations of Theme
, and the instances of the refined abstraction of Component
is created. Then, the concrete theme implementation is passed as a parameter to the concrete refined abstraction.
// Client Code
const lightTheme: Theme = new LightTheme()
const lightButton: Component = new Button(lightTheme)
lightButton.render()
All combination of any element and any theme is possible, still easy to manage complexity and facilitate future modifications in the design system.
const darkTheme: Theme = new DarkTheme()
const darkDialog: Component = new Dialog(darkTheme)
darkDialog.render()
const highContrastTheme: Theme = new HighContrastTheme()
const highContrastForm: Component = new Form(highContrastTheme)
highContrastForm.render()
🧑🏻💻 Use it or Avoid it
When to use it
This is especially useful if there are multiple variations or dimensions of change and you anticipate different variations or extensions in both the abstraction
and implementation
hierarchies.
If you want the flexibility to switch implementations at runtime, this pattern can be a good choice.
When to avoid it
If you have a straightforward scenario with only one abstraction
and one corresponding implementation
, introducing the Bridge Pattern is unnecessary.
Also, if the abstraction
and implementation
are tightly coupled so that they are unlikely to change independently, usingthis pattern might introduce unnecessary complexity without clear benefits.
In short, the Bridge Pattern helps to manage complexity, improve flexibility by decoupling abstractions from their implementations.