Bridge Design Pattern (Structural)
Bridge 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.
Problem
Section titled βProblemβSuppose you have a geometric Shape class with a pair of subclasses: Circle and Square. You want to extend this class hierarchy to incorporate colors, so you plan to create Red and Blue shape subclasses. However, since you already have two subclasses, youβll need to create four class combinations such as BlueCircle and RedSquare.
Adding new shape types and colors to the hierarchy will grow it exponentially. For instance, to add a triangle shape youβd need to introduce two subclasses, one for each color. And after that, adding a new color would require creating three subclasses, one for each shape type. The further we go, the worse it becomes.
Solution
Section titled βSolutionβThis problem occurs because weβre trying to extend the shape classes in two independent dimensions: by form and by color. Thatβs a very common issue with class inheritance.
The Bridge pattern suggests that you switch from inheritance to object composition. What this means is that you extract one of the dimensions into a separate class hierarchy, so that the original classes will reference an object of the new hierarchy, instead of having all of its state and behaviors within one class.
Using this approach, we can extract the color-related code into its own class with two subclasses: Red and Blue. The Shape class then gets a reference field pointing to one of the color objects. Now the shape can delegate any color-related work to the linked color object. That reference will act as a bridge between the Shape and Color classes. From now on, adding new colors wonβt require changing the shape hierarchy, and vice versa.
Abstraction and Implementation
Section titled βAbstraction and ImplementationβThe GoF design patterns book introduces the terms Abstraction and Implementation as part of the Bridge definition.
- Abstraction (also called interface) is a high-level control layer for some entity. This layer isnβt supposed to do any real work on its own. It should delegate the work to the implementation layer (also called platform).
- Implementation is the actual code that performs the work.
Structure (Class Diagram)
Section titled βStructure (Class Diagram)βclassDiagram
class Abstraction {
- implementation: Implementation
+ feature1()
+ feature2()
}
class RefinedAbstraction {
+ featureN()
}
class Implementation {
<<interface>>
+ method1()
+ method2()
}
class ConcreteImplementationA {
+ method1()
+ method2()
}
class ConcreteImplementationB {
+ method1()
+ method2()
}
Abstraction o-- Implementation : bridge
Abstraction <|-- RefinedAbstraction
Implementation <|.. ConcreteImplementationA
Implementation <|.. ConcreteImplementationB
Pseudocode
Section titled βPseudocodeβ# The "abstraction" defines the interface for the "control"# part of the two class hierarchies. It maintains a reference# to an object of the "implementation" hierarchy.class RemoteControl: protected device: Device constructor RemoteControl(device: Device): this.device = device method togglePower(): if (device.isEnabled()): device.disable() else: device.enable()
# You can extend classes from the abstraction hierarchy# independently from device classes.class AdvancedRemoteControl extends RemoteControl: method mute(): device.setVolume(0)
# The "implementation" interface declares methods common to all# concrete implementation classes.interface Device: method isEnabled() method enable() method disable() method setVolume(percent)
# Concrete implementations.class Tv implements Device: # ... implementations of methods ...
class Radio implements Device: # ... implementations of methods ...Applicability
Section titled βApplicabilityβ- Use the Bridge pattern when you want to divide and organize a monolithic class that has several variants of some functionality (for example, if the class can work with various database servers).
- Use the pattern when you need to extend a class in several orthogonal (independent) dimensions.
- Use the Bridge if you need to be able to switch implementations at runtime.
How to Implement
Section titled βHow to Implementβ- Identify the independent dimensions in your classes. These independent concepts could be: abstraction/platform, domain/infrastructure, front-end/back-end, or interface/implementation.
- See what operations the client needs and define them in the base abstraction class.
- Determine the operations available on all platforms. Declare the ones that the abstraction needs in the general implementation interface.
- For all platforms in your domain create concrete implementation classes, but make sure they all follow the implementation interface.
- Inside the abstraction class, add a reference field for the implementation type. The abstraction delegates most of the work to the implementation object thatβs referenced in that field.
- If you have several variants of high-level logic, create refined abstractions for each variant by extending the base abstraction class.
- The client code should pass an implementation object to the abstractionβs constructor to associate one with the other. After that, the client can forget about the implementation and work only with the abstraction object.
Pros and Cons
Section titled βPros and ConsβPros:
- You can create platform-independent classes and apps.
- The client code works with high-level abstractions. It isnβt exposed to the platform details.
- Open/Closed Principle. You can introduce new abstractions and implementations independently from each other.
- Single Responsibility Principle. You can focus on high-level logic in the abstraction and on platform details in the implementation.
Cons:
- You might make the code more complicated by applying the pattern to a highly cohesive class.
Relations with other Design Patterns
Section titled βRelations with other Design Patternsβ- Bridge is usually designed up-front, letting you develop parts of an application independently of each other. On the other hand, Adapter is commonly used with an existing app to make some otherwise-incompatible classes work together nicely.
- Bridge, State, Strategy (and to some degree Adapter) have very similar structures. Indeed, all of these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isnβt just a recipe for structuring your code. It can also communicate to other developers the problem the pattern solves.
Code Examples
Section titled βCode ExamplesβCode Examples (C#)
Section titled βCode Examples (C#)β// Implementerpublic interface IRenderer{ void RenderCircle(float radius);}
// Concrete Implementerspublic class VectorRenderer : IRenderer{ public void RenderCircle(float radius) => Console.WriteLine($"Draw a circle in vector format with radius {radius}");}
public class RasterRenderer : IRenderer{ public void RenderCircle(float radius) => Console.WriteLine($"Draw a circle in raster with radius {radius}");}
// Abstractionpublic abstract class Shape{ protected readonly IRenderer _renderer;
protected Shape(IRenderer renderer) { _renderer = renderer; }
public abstract void Draw();}
// Refined Abstractionpublic class Circle : Shape{ private readonly float _radius;
public Circle(IRenderer renderer, float radius) : base(renderer) { _radius = radius; }
public override void Draw() { _renderer.RenderCircle(_radius); }}
// Usageclass Program{ static void Main() { var vector = new VectorRenderer(); var raster = new RasterRenderer();
Shape circle1 = new Circle(vector, 5); Shape circle2 = new Circle(raster, 10);
circle1.Draw(); circle2.Draw(); }}Code Examples (Python)
Section titled βCode Examples (Python)βfrom __future__ import annotationsfrom abc import ABC, abstractmethod
class Abstraction: def __init__(self, implementation: Implementation) -> None: self.implementation = implementation
def operation(self) -> str: return (f"Abstraction: Base operation with:\n" f"{self.implementation.operation_implementation()}")
class ExtendedAbstraction(Abstraction): def operation(self) -> str: return (f"ExtendedAbstraction: Extended operation with:\n" f"{self.implementation.operation_implementation()}")
class Implementation(ABC): @abstractmethod def operation_implementation(self) -> str: pass
class ConcreteImplementationA(Implementation): def operation_implementation(self) -> str: return "ConcreteImplementationA: Here's the result on the platform A."
class ConcreteImplementationB(Implementation): def operation_implementation(self) -> str: return "ConcreteImplementationB: Here's the result on the platform B."
def client_code(abstraction: Abstraction) -> None: print(abstraction.operation(), end="")
if __name__ == "__main__": implementation = ConcreteImplementationA() abstraction = Abstraction(implementation) client_code(abstraction)
print("\n")
implementation = ConcreteImplementationB() abstraction = ExtendedAbstraction(implementation) client_code(abstraction)Code Examples (C++)
Section titled βCode Examples (C++)β#include <iostream>
// Implementerclass IRenderer {public: virtual ~IRenderer() = default; virtual void RenderCircle(float radius) = 0;};
// Concrete Implementersclass VectorRenderer : public IRenderer {public: void RenderCircle(float radius) override { std::cout << "Draw a circle in vector format with radius " << radius << std::endl; }};
class RasterRenderer : public IRenderer {public: void RenderCircle(float radius) override { std::cout << "Draw a circle in raster with radius " << radius << std::endl; }};
// Abstractionclass Shape {protected: IRenderer& renderer;
public: Shape(IRenderer& r) : renderer(r) {} virtual ~Shape() = default;
virtual void Draw() = 0;};
// Refined Abstractionclass Circle : public Shape {private: float radius;
public: Circle(IRenderer& r, float rad) : Shape(r), radius(rad) {}
void Draw() override { renderer.RenderCircle(radius); }};
// Usageint main() { VectorRenderer vector; RasterRenderer raster;
Shape* circle1 = new Circle(vector, 5); Shape* circle2 = new Circle(raster, 10);
circle1->Draw(); circle2->Draw();
delete circle1; delete circle2;
return 0;}Code Examples (Java)
Section titled βCode Examples (Java)β// Implementerpublic interface IRenderer { void renderCircle(float radius);}
// Concrete Implementerspublic class VectorRenderer implements IRenderer { @Override public void renderCircle(float radius) { System.out.println("Draw a circle in vector format with radius " + radius); }}
public class RasterRenderer implements IRenderer { @Override public void renderCircle(float radius) { System.out.println("Draw a circle in raster with radius " + radius); }}
// Abstractionpublic abstract class Shape {
protected final IRenderer renderer;
protected Shape(IRenderer renderer) { this.renderer = renderer; }
public abstract void draw();}
// Refined Abstractionpublic class Circle extends Shape {
private final float radius;
public Circle(IRenderer renderer, float radius) { super(renderer); this.radius = radius; }
@Override public void draw() { renderer.renderCircle(radius); }}
// Usagepublic class Program { public static void main(String[] args) { IRenderer vector = new VectorRenderer(); IRenderer raster = new RasterRenderer();
Shape circle1 = new Circle(vector, 5); Shape circle2 = new Circle(raster, 10);
circle1.draw(); circle2.draw(); }}Code Examples (JavaScript)
Section titled βCode Examples (JavaScript)β// Implementer (simulated interface)class IRenderer { renderCircle(radius) { throw new Error("renderCircle() must be implemented"); }}
// Concrete Implementersclass VectorRenderer extends IRenderer { renderCircle(radius) { console.log(`Draw a circle in vector format with radius ${radius}`); }}
class RasterRenderer extends IRenderer { renderCircle(radius) { console.log(`Draw a circle in raster with radius ${radius}`); }}
// Abstractionclass Shape { constructor(renderer) { if (new.target === Shape) { throw new Error("Shape is abstract and cannot be instantiated directly"); } this.renderer = renderer; }
draw() { throw new Error("draw() must be implemented"); }}
// Refined Abstractionclass Circle extends Shape { constructor(renderer, radius) { super(renderer); this.radius = radius; }
draw() { this.renderer.renderCircle(this.radius); }}
// Usageconst vector = new VectorRenderer();const raster = new RasterRenderer();
const circle1 = new Circle(vector, 5);const circle2 = new Circle(raster, 10);
circle1.draw();circle2.draw();Code Examples (Visual Basic)
Section titled βCode Examples (Visual Basic)β' ============================' Implementer' ============================Public Interface IRenderer Sub RenderCircle(radius As Single)End Interface
' ============================' Concrete Implementers' ============================Public Class VectorRenderer Implements IRenderer
Public Sub RenderCircle(radius As Single) Implements IRenderer.RenderCircle Console.WriteLine($"Draw a circle in vector format with radius {radius}") End SubEnd Class
Public Class RasterRenderer Implements IRenderer
Public Sub RenderCircle(radius As Single) Implements IRenderer.RenderCircle Console.WriteLine($"Draw a circle in raster with radius {radius}") End SubEnd Class
' ============================' Abstraction' ============================Public MustInherit Class Shape Protected ReadOnly _renderer As IRenderer
Protected Sub New(renderer As IRenderer) _renderer = renderer End Sub
Public MustOverride Sub Draw()End Class
' ============================' Refined Abstraction' ============================Public Class Circle Inherits Shape
Private ReadOnly _radius As Single
Public Sub New(renderer As IRenderer, radius As Single) MyBase.New(renderer) _radius = radius End Sub
Public Overrides Sub Draw() _renderer.RenderCircle(_radius) End SubEnd Class
' ============================' Usage' ============================Module Program Sub Main() Dim vector As New VectorRenderer() Dim raster As New RasterRenderer()
Dim circle1 As Shape = New Circle(vector, 5) Dim circle2 As Shape = New Circle(raster, 10)
circle1.Draw() circle2.Draw() End SubEnd ModuleCode Examples (Rust)
Section titled βCode Examples (Rust)β// Implementertrait Renderer { fn render_circle(&self, radius: f32);}
// Concrete Implementersstruct VectorRenderer;
impl Renderer for VectorRenderer { fn render_circle(&self, radius: f32) { println!("Draw a circle in vector format with radius {}", radius); }}
struct RasterRenderer;
impl Renderer for RasterRenderer { fn render_circle(&self, radius: f32) { println!("Draw a circle in raster with radius {}", radius); }}
// Abstractiontrait Shape { fn draw(&self);}
// Refined Abstractionstruct Circle { renderer: Box<dyn Renderer>, radius: f32,}
impl Circle { fn new(renderer: Box<dyn Renderer>, radius: f32) -> Self { Self { renderer, radius } }}
impl Shape for Circle { fn draw(&self) { self.renderer.render_circle(self.radius); }}
// Usagefn main() { let vector = Box::new(VectorRenderer); let raster = Box::new(RasterRenderer);
let circle1 = Circle::new(vector, 5.0); let circle2 = Circle::new(raster, 10.0);
circle1.draw(); circle2.draw();}Code Examples (PHP)
Section titled βCode Examples (PHP)β<?php
// Implementerinterface IRenderer{ public function renderCircle(float $radius): void;}
// Concrete Implementersclass VectorRenderer implements IRenderer{ public function renderCircle(float $radius): void { echo "Draw a circle in vector format with radius {$radius}\n"; }}
class RasterRenderer implements IRenderer{ public function renderCircle(float $radius): void { echo "Draw a circle in raster with radius {$radius}\n"; }}
// Abstractionabstract class Shape{ protected IRenderer $renderer;
public function __construct(IRenderer $renderer) { $this->renderer = $renderer; }
abstract public function draw(): void;}
// Refined Abstractionclass Circle extends Shape{ private float $radius;
public function __construct(IRenderer $renderer, float $radius) { parent::__construct($renderer); $this->radius = $radius; }
public function draw(): void { $this->renderer->renderCircle($this->radius); }}
// Usage$vector = new VectorRenderer();$raster = new RasterRenderer();
$circle1 = new Circle($vector, 5);$circle2 = new Circle($raster, 10);
$circle1->draw();$circle2->draw();Code Examples (Go)
Section titled βCode Examples (Go)βpackage main
import ( "fmt")
// Implementertype Renderer interface { RenderCircle(radius float32)}
// Concrete Implementerstype VectorRenderer struct{}
func (VectorRenderer) RenderCircle(radius float32) { fmt.Printf("Draw a circle in vector format with radius %.2f\n", radius)}
type RasterRenderer struct{}
func (RasterRenderer) RenderCircle(radius float32) { fmt.Printf("Draw a circle in raster with radius %.2f\n", radius)}
// Abstractiontype Shape interface { Draw()}
// Refined Abstractiontype Circle struct { renderer Renderer radius float32}
func NewCircle(renderer Renderer, radius float32) *Circle { return &Circle{ renderer: renderer, radius: radius, }}
func (c *Circle) Draw() { c.renderer.RenderCircle(c.radius)}
// Usagefunc main() { vector := VectorRenderer{} raster := RasterRenderer{}
circle1 := NewCircle(vector, 5) circle2 := NewCircle(raster, 10)
circle1.Draw() circle2.Draw()}Code Examples (Ruby)
Section titled βCode Examples (Ruby)β# Implementerclass Rendererdef render_circle(radius) raise NotImplementedError, "Subclasses must implement render_circle"endend
# Concrete Implementersclass VectorRenderer < Rendererdef render_circle(radius) puts "Draw a circle in vector format with radius #{radius}"endend
class RasterRenderer < Rendererdef render_circle(radius) puts "Draw a circle in raster with radius #{radius}"endend
# Abstractionclass Shapedef initialize(renderer) @renderer = rendererend
def draw raise NotImplementedError, "Subclasses must implement draw"endend
# Refined Abstractionclass Circle < Shapedef initialize(renderer, radius) super(renderer) @radius = radiusend
def draw @renderer.render_circle(@radius)endend
# Usagevector = VectorRenderer.newraster = RasterRenderer.new
circle1 = Circle.new(vector, 5)circle2 = Circle.new(raster, 10)
circle1.drawcircle2.drawCode Examples (Perl)
Section titled βCode Examples (Perl)β#!/usr/bin/perluse strict;use warnings;
############################# Implementer (Interface-like)############################package Renderer;
sub render_circle { die "render_circle() must be implemented by subclass";}
############################# Concrete Implementers############################package VectorRenderer;our @ISA = ('Renderer');
sub new { bless {}, shift }
sub render_circle { my ($self, $radius) = @_; print "Draw a circle in vector format with radius $radius\n";}
package RasterRenderer;our @ISA = ('Renderer');
sub new { bless {}, shift }
sub render_circle { my ($self, $radius) = @_; print "Draw a circle in raster with radius $radius\n";}
############################# Abstraction############################package Shape;
sub new { my ($class, $renderer) = @_; return bless { renderer => $renderer }, $class;}
sub draw { die "draw() must be implemented by subclass";}
############################# Refined Abstraction############################package Circle;our @ISA = ('Shape');
sub new { my ($class, $renderer, $radius) = @_; return bless { renderer => $renderer, radius => $radius }, $class;}
sub draw { my ($self) = @_; $self->{renderer}->render_circle($self->{radius});}
############################# Usage############################package main;
my $vector = VectorRenderer->new();my $raster = RasterRenderer->new();
my $circle1 = Circle->new($vector, 5);my $circle2 = Circle->new($raster, 10);
$circle1->draw();$circle2->draw();Code Examples (TypeScript)
Section titled βCode Examples (TypeScript)β// Implementerinterface IRenderer { renderCircle(radius: number): void;}
// Concrete Implementersclass VectorRenderer implements IRenderer { renderCircle(radius: number): void { console.log(`Draw a circle in vector format with radius ${radius}`); }}
class RasterRenderer implements IRenderer { renderCircle(radius: number): void { console.log(`Draw a circle in raster with radius ${radius}`); }}
// Abstractionabstract class Shape { protected renderer: IRenderer;
constructor(renderer: IRenderer) { this.renderer = renderer; }
abstract draw(): void;}
// Refined Abstractionclass Circle extends Shape { private radius: number;
constructor(renderer: IRenderer, radius: number) { super(renderer); this.radius = radius; }
draw(): void { this.renderer.renderCircle(this.radius); }}
// Usageconst vector = new VectorRenderer();const raster = new RasterRenderer();
const circle1: Shape = new Circle(vector, 5);const circle2: Shape = new Circle(raster, 10);
circle1.draw();circle2.draw();