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.
Labels with a subclass for PDF
ShippingLabel and ShelfLabel print thermal commands. PdfShippingLabel and PdfShelfLabel override print() for PDF.
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.
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();
}
}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:
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.
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 + "\"");
}
}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 ||||
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.
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 |
| The high-level class clients use; holds the implementor |
Refined abstraction |
| Variations of the abstraction, written in terms of the implementor |
Implementor |
| The low-level operations every platform must provide |
Concrete implementor |
| 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()andbarcode(). 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()andbarcode()are drawing operations. AprintShippingLabel()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 |
JavaScript | Any object with the primitive methods, passed to the constructor |
Python | An object matching a |
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
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.