If you’ve ever started sketching out a class hierarchy and realized halfway through that you’re about to multiply two unrelated things together, this is for you. The vehicle workshop example here makes it concrete: four vehicle types, four workshop operations, and if you inherit your way through it you end up writing CarProduce, CarAssemble, BikeProduce, BikeAssemble, and so on until you’ve written sixteen classes for what’s really two independent lists of four things each.
The problem
You’ve got two dimensions of variation that both need to grow independently. Every time inheritance is the tool you reach for in this situation, you get a class per combination, and every new vehicle type or every new workshop operation multiplies the class count instead of adding to it.
Without the pattern
The instinct that gets you into trouble is folding both axes into one inheritance tree. Say you skip the Workshop interface entirely and just give Vehicle an abstract performOperation(), letting each vehicle type override it per operation it needs. Now Car needs a produce step, an assemble step, a paint step, and an inspect step, and since a single override can’t cleanly be “four different methods depending on context,” you end up carving out a subclass per operation instead: CarProduce, CarAssemble, CarPaint, CarInspect. Fine, that’s one vehicle handled. Then Bike needs the same four operations, so you write BikeProduce, BikeAssemble, BikePaint, BikeInspect, and by the time Truck and Motorcycle show up you’ve written sixteen classes to express what’s really a 4-by-4 grid of two independent lists.
classDiagram
class Vehicle {
<<abstract>>
}
Vehicle <|-- CarProduce
Vehicle <|-- CarAssemble
Vehicle <|-- CarPaint
Vehicle <|-- CarInspect
Vehicle <|-- BikeProduce
Vehicle <|-- BikeAssemble
Vehicle <|-- BikePaint
Vehicle <|-- BikeInspect
Vehicle <|-- TruckProduce
Vehicle <|-- TruckAssemble
Vehicle <|-- TruckPaint
Vehicle <|-- TruckInspect
Vehicle <|-- MotorcycleProduce
Vehicle <|-- MotorcycleAssemble
Vehicle <|-- MotorcyclePaint
Vehicle <|-- MotorcycleInspect
The tell isn’t the number sixteen itself, it’s what happens when either axis grows by one. Add a fifth vehicle, Bus, and you’re not writing one new class, you’re writing four, one per existing operation. Add a fifth operation, Repair, and you’re not writing one new class either, you’re writing four, one per existing vehicle. The two axes were never actually coupled, a workshop doesn’t care what it’s servicing and a vehicle doesn’t care how it’s serviced, but jamming them into a single hierarchy makes every future addition on either side pay for both.
With the pattern
Workshop is the implementor interface: work() and getWorkshopType(). Produce, Assemble, Paint, and Inspect are the concrete implementors, each just printing what it does and naming itself.
Vehicle is the abstraction, an abstract class holding two Workshop references as protected fields, workshop1 and workshop2, set through the constructor. Its manufacture() method is a small template method: print a start message, call performWorkshop1(), call performWorkshop2(), print a completion message. Those two performWorkshopN() methods just delegate to workshop1.work() and workshop2.work(). Vehicle also declares getVehicleType() as abstract, so Car, Bike, Truck, and Motorcycle only need to implement that one method, they inherit everything else.
The key decision is that Vehicle holds Workshop by composition, not by extending some CarWorkshop base. Any vehicle can be constructed with any pair of workshops, new Car(produce, assemble) and new Car(paint, inspect) are both valid without adding a single class. The vehicle hierarchy and the workshop hierarchy know nothing about each other beyond the interface, so you can extend either one without touching the other. Add a Bus vehicle, it works with all four existing workshops immediately. Add a Repair workshop, all four existing vehicles can use it immediately.
classDiagram
class Workshop {
<<interface>>
+work()
+getWorkshopType() String
}
class Produce {
+work()
+getWorkshopType() String
}
class Assemble {
+work()
+getWorkshopType() String
}
class Paint {
+work()
+getWorkshopType() String
}
class Inspect {
+work()
+getWorkshopType() String
}
class Vehicle {
<<abstract>>
#workshop1: Workshop
#workshop2: Workshop
+manufacture()
#performWorkshop1()
#performWorkshop2()
+getVehicleType()* String
+displayConfiguration()
}
class Car {
+getVehicleType() String
}
class Bike {
+getVehicleType() String
}
class Truck {
+getVehicleType() String
}
class Motorcycle {
+getVehicleType() String
}
Workshop <|.. Produce
Workshop <|.. Assemble
Workshop <|.. Paint
Workshop <|.. Inspect
Vehicle <|-- Car
Vehicle <|-- Bike
Vehicle <|-- Truck
Vehicle <|-- Motorcycle
Vehicle o-- Workshop
What it costs you
Bridge fixes the explosion, but it isn’t free. You’re now maintaining two hierarchies instead of one, Vehicle and its subclasses on one side, Workshop and its subclasses on the other, and reasoning about a Car built with Paint and Inspect means holding both trees in your head at once instead of just one. Every manufacture() call also pays an indirection hop the naive version didn’t have: performWorkshop1() doesn’t do the work itself, it calls workshop1.work() through a field reference, so you’re chasing composition instead of hitting a method directly on the object. And it’s premature if the implementation side never actually varies, if every vehicle in your system always gets the same four workshop steps in the same order, splitting Workshop out as its own hierarchy just gives you two class trees to update every time you touch what used to be one thing. Bridge earns its cost when both axes genuinely move independently, not because two hierarchies sounds more architecturally sound than one.
When to reach for it
- You have two hierarchies that both want to grow, and inheriting through both at once multiplies your class count.
- You want to pick or swap the implementation side at runtime, not bake it in at compile time.
- You’re designing this upfront, this isn’t a retrofit pattern, that’s Adapter’s job.
- The implementation side is a set of interchangeable single things, not a matched family of objects. If it’s a family (all-Motif widgets, all-Mac widgets that must stay consistent), that’s Abstract Factory’s job instead, the two get compared in Designing a Document Editor.
The takeaway
Bridge is what you reach for before the class explosion happens, not after. If you’re already staring at a naming scheme like CarProduce and BikeAssemble, that’s the signal you needed this two designs ago.
Read the full source on GitHub.
Comments