Low-level designDeep dive8 min

Bridge

When a class varies in two independent ways, give each way its own hierarchy and let one hold the other. Two hierarchies of n and m classes replace one of n × m, and each side can grow without touching the other.

Watch it evolve1 / 8
The problem

Labels with a subclass for PDF

ShippingLabel and ShelfLabel print thermal commands. PdfShippingLabel and PdfShelfLabel override print() for PDF.

80
Start
Stage 1
Stage 2
Final

1 · The situation

Labels, and the printers they go to

A warehouse prints two kinds of label: a ShippingLabel with the recipient and a tracking barcode, and a ShelfLabel with a product name and its SKU barcode. They started on the warehouse's thermal printers, which take short text commands: T for a line of text and B for a barcode, each at a position in millimeters.

Last month the office asked for PDF labels too. Each label already knew how to print itself, so each got a subclass that prints the same layout as PDF instructions: PdfShippingLabel and PdfShelfLabel. The command formats in this sample are simplified stand-ins for real printer languages.

The current versionJava
abstract class Label {
    abstract void print();
}

class ShippingLabel extends Label {
    protected final String recipient;
    protected final String tracking;

    ShippingLabel(String recipient, String tracking) {
        this.recipient = recipient;
        this.tracking = tracking;
    }

    void print() {
        System.out.println("  T 5,5 \"TO " + recipient + "\"");
        System.out.println("  B 5,20 \"" + tracking + "\"");
    }
}

class ShelfLabel extends Label {
    protected final String product;
    protected final String sku;

    ShelfLabel(String product, String sku) {
        this.product = product;
        this.sku = sku;
    }

    void print() {
        System.out.println("  T 5,5 \"" + product + "\"");
        System.out.println("  B 5,15 \"" + sku + "\"");
    }
}

class PdfShippingLabel extends ShippingLabel {
    PdfShippingLabel(String recipient, String tracking) {
        super(recipient, tracking);
    }

    @Override
    void print() {
        System.out.println("  text at 5mm, 5mm: TO " + recipient);
        System.out.println("  barcode at 5mm, 20mm: " + tracking);
    }
}

class PdfShelfLabel extends ShelfLabel {
    PdfShelfLabel(String product, String sku) {
        super(product, sku);
    }

    @Override
    void print() {
        System.out.println("  text at 5mm, 5mm: " + product);
        System.out.println("  barcode at 5mm, 15mm: " + sku);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("Thermal:");
        new ShippingLabel("Ana Ruiz", "TRK-4471").print();
        new ShelfLabel("Desk lamp", "SKU-208").print();
        System.out.println("PDF:");
        new PdfShippingLabel("Ana Ruiz", "TRK-4471").print();
        new PdfShelfLabel("Desk lamp", "SKU-208").print();
    }
}
«abstract»Label~ print()ShippingLabel# recipient: String# tracking: String~ print()ShelfLabel# product: String# sku: String~ print()PdfShippingLabel~ print()PdfShelfLabel~ print()

The class diagram of this version, drawn from its code: each output format is a subclass of each kind of label.

Both labels, on both outputs:

Thermal:
  T 5,5 "TO Ana Ruiz"
  B 5,20 "TRK-4471"
  T 5,5 "Desk lamp"
  B 5,15 "SKU-208"
PDF:
  text at 5mm, 5mm: TO Ana Ruiz
  barcode at 5mm, 20mm: TRK-4471
  text at 5mm, 5mm: Desk lamp
  barcode at 5mm, 15mm: SKU-208

2 · The change

A third output, and a third label

The website now wants a screen preview of every label, and the returns team wants a ReturnLabel. Look at what one layout costs today:

The same layout, written once per outputJava
class ShippingLabel extends Label {
    protected final String recipient;
    protected final String tracking;

    ShippingLabel(String recipient, String tracking) {
        this.recipient = recipient;
        this.tracking = tracking;
    }

    void print() {
        System.out.println("  T 5,5 \"TO " + recipient + "\"");
        System.out.println("  B 5,20 \"" + tracking + "\"");
    }
}

class PdfShippingLabel extends ShippingLabel {
    PdfShippingLabel(String recipient, String tracking) {
        super(recipient, tracking);
    }

    @Override
    void print() {
        System.out.println("  text at 5mm, 5mm: TO " + recipient);
        System.out.println("  barcode at 5mm, 20mm: " + tracking);
    }
}

Each class mixes two decisions: what goes on a label, and how the output device draws it. Inheritance can only split a class one way, so every combination of the two needs a class of its own:

Labels

Outputs

Classes needed by subclassing

2: shipping, shelf

2: thermal, PDF

4

2

3: plus the screen preview

6

3: plus returns

3

9

Worse than the count is the duplication. The shipping layout, recipient at 5,5 and barcode at 5,20, is written in every output's class for it. Move the barcode and you edit three classes; add a field to the shelf label and you edit three more, and nothing makes them stay in step.

3 · The refactor

Split the two decisions into two hierarchies

Start from the current version. What varies between outputs is only how text and barcodes are drawn. Pull that out behind an interface, and have each label use it:

LabelPrinter has two operations every output needs, text(x, y, s) and barcode(x, y, code). ThermalPrinter and PdfPrinter implement them, each in its own format.

Java
interface LabelPrinter {
    void text(int x, int y, String s);
    void barcode(int x, int y, String code);
}

class ThermalPrinter implements LabelPrinter {
    public void text(int x, int y, String s) {
        System.out.println("  T " + x + "," + y + " \"" + s + "\"");
    }

    public void barcode(int x, int y, String code) {
        System.out.println("  B " + x + "," + y + " \"" + code + "\"");
    }
}
«interface»LabelPrinter+ text(int, int, String)+ barcode(int, int, String)ThermalPrinter+ text(int, int, String)+ barcode(int, int, String)PdfPrinter+ text(int, int, String)+ barcode(int, int, String)«abstract»Label# printer: LabelPrinter~ print()ShippingLabel− recipient: String− tracking: String~ print()ShelfLabel− product: String− sku: String~ print()

After the second step the program prints exactly what it printed before. After the third, every label also has a preview:

Thermal:
  T 5,5 "TO Ana Ruiz"
  B 5,20 "TRK-4471"
  T 5,5 "Desk lamp"
  B 5,15 "SKU-208"
PDF:
  text at 5mm, 5mm: TO Ana Ruiz
  barcode at 5mm, 20mm: TRK-4471
  text at 5mm, 5mm: Desk lamp
  barcode at 5mm, 15mm: SKU-208
Screen preview:
  [5,5] TO Ana Ruiz
  [5,20] |||| TRK-4471 ||||
  [5,5] Desk lamp
  [5,15] |||| SKU-208 ||||
Subclassing, 2 × 2
4
Two hierarchies, 2 + 2
4
Subclassing, 3 × 3
9
Two hierarchies, 3 + 3
6

Concrete classes for labels and outputs. Subclassing multiplies the two; two hierarchies add them.

4 · The pattern

Bridge: an abstraction and its implementation, apart

Bridge separates a class's abstraction, what it is and does, from its implementation, how the work is carried out on some platform, into two hierarchies joined by a reference. The abstraction side holds an implementor and makes every platform-specific call through it, so each side can gain subclasses without the other changing.

«interface»LabelPrinter+ text(int, int, String)+ barcode(int, int, String)ThermalPrinter+ text(int, int, String)+ barcode(int, int, String)PdfPrinter+ text(int, int, String)+ barcode(int, int, String)ScreenPreview+ text(int, int, String)+ barcode(int, int, String)«abstract»Label# printer: LabelPrinter~ print()ShippingLabel− recipient: String− tracking: String~ print()ShelfLabel− product: String− sku: String~ print()

The two hierarchies in the warehouse's code, and the reference that joins them: Label holds a LabelPrinter.

Role

In the warehouse

Its job

Abstraction

Label

The high-level class clients use; holds the implementor

Refined abstraction

ShippingLabel, ShelfLabel

Variations of the abstraction, written in terms of the implementor

Implementor

LabelPrinter

The low-level operations every platform must provide

Concrete implementor

ThermalPrinter, PdfPrinter, ScreenPreview

One platform each

The label decides what goes where; the printer decides how it is drawn.

The implementor's operations are primitive on purpose: draw text, draw a barcode. The abstraction combines them into something meaningful, a shipping label. That division is what keeps the two sides independent: a new label needs no new primitive, and a new output needs no knowledge of labels.

5 · What it costs

An interface that must serve every platform

  • The implementor interface is hard to change later. Every output implements text() and barcode(). If a new label needs a QR code, every printer class changes.
  • Lowest common denominator. The interface can only offer what every platform can do, so a printer's special features, such as a thermal printer's built-in fonts, are out of reach unless the interface grows.
  • Indirection. Reading what a PDF shipping label looks like means reading two classes instead of one.
  • It's only worth it with two real dimensions. With one output, a plain class hierarchy is simpler.

Situation

Bridge?

Two independent things vary, and both grow

Yes: two hierarchies

Only one thing varies

No: a single hierarchy, or a Strategy

The second dimension is one setting, such as a page size

No: a constructor parameter

Platform code must be swapped at run time, or kept out of the abstraction's build

Yes: the abstraction knows only the interface

6 · Getting it right

Primitive operations on one side, meaning on the other

  • Keep the implementor's operations primitive. text() and barcode() are drawing operations. A printShippingLabel() on the printer would pull label knowledge back into every output.
  • Pass the implementor in. A label is given its printer, so the same label object can be printed anywhere, and tests can use a recording printer.
  • Grow the interface on purpose. When a new primitive is needed, add it to every implementor in the same change, or give it a default that falls back to existing primitives.
  • Design it early when you can. Bridge is easiest to set up before the subclasses multiply; afterward, as here, it's a refactor that deletes classes.

Language

The reference between the hierarchies

Java

A field of the implementor interface, set in the abstraction's constructor

C++

A reference or std::unique_ptr to an abstract class; the same shape, sometimes called pimpl, also hides platform headers from the abstraction's users

JavaScript

Any object with the primitive methods, passed to the constructor

Python

An object matching a Protocol, passed to the constructor

7 · Look-alikes

One reference, different reasons

Bridge's structure, a class holding an interface, is shared with several patterns. What differs is why the reference is there:

Pattern

Why one object holds another

Designed

Bridge

To let an abstraction and its platform vary independently, as two hierarchies

Up front, or as a refactor of a class explosion

Strategy

To swap one way of doing a single job

Around one algorithm

Adapter

To make an existing class fit an interface it wasn't written for

After the fact, to join things that already exist

Abstract Factory

To create the matching set of platform objects

Often used to create a Bridge's implementor

Bridge and Strategy are the closest: both hold an interface and delegate to it. A strategy is one interchangeable algorithm used by one class; a bridge's implementor is a whole platform layer under a hierarchy of abstractions that keeps growing on its own.

8 · In practice

Bridges you have already used

Java's JDBC works this way. Programs are written against the JDBC interfaces, and each database vendor supplies a driver class implementing java.sql.Driver; the DriverManager asks each registered driver in turn to connect. The same application code then runs on any database with a driver, and a new driver needs no change to applications.

The practice checks have no Bridge detector. Their reading of this page's code is a likely Command, from the abstract Label with one print() method; that's a limit of the checks, so this page shows no checks card.

Check yourself

4 questions

1. The warehouse adds a ReturnLabel after the refactor. What changes?
2. A new label needs a QR code. Which side pays?
3. How does Bridge differ from Strategy?
4. Why are text() and barcode() better than printShippingLabel() on the printer?

Takeaways

Remember this

  • Bridge separates what a class does from how a platform carries it out, as two hierarchies.
  • Subclassing two independent dimensions multiplies classes; two hierarchies only add them.
  • The abstraction holds an implementor and makes every platform call through it.
  • Keep the implementor's operations primitive, and put the meaning in the abstraction.
  • Bridge and Strategy share a shape; a bridge spans a whole hierarchy and its platform.