Skip to content

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.

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.

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.

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.
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
# 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 ...
  • 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.
  1. Identify the independent dimensions in your classes. These independent concepts could be: abstraction/platform, domain/infrastructure, front-end/back-end, or interface/implementation.
  2. See what operations the client needs and define them in the base abstraction class.
  3. Determine the operations available on all platforms. Declare the ones that the abstraction needs in the general implementation interface.
  4. For all platforms in your domain create concrete implementation classes, but make sure they all follow the implementation interface.
  5. 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.
  6. If you have several variants of high-level logic, create refined abstractions for each variant by extending the base abstraction class.
  7. 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:

  • 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.
  • 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.

// Implementer
public interface IRenderer
{
void RenderCircle(float radius);
}
// Concrete Implementers
public 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}");
}
// Abstraction
public abstract class Shape
{
protected readonly IRenderer _renderer;
protected Shape(IRenderer renderer)
{
_renderer = renderer;
}
public abstract void Draw();
}
// Refined Abstraction
public class Circle : Shape
{
private readonly float _radius;
public Circle(IRenderer renderer, float radius) : base(renderer)
{
_radius = radius;
}
public override void Draw()
{
_renderer.RenderCircle(_radius);
}
}
// Usage
class 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();
}
}