Appearance
Bridge
Separate the abstraction from its implementation so that both can vary independently.
The definition of the Bridge pattern is quite abstract and not easy to understand directly, so let’s illustrate it with an example.
Suppose a car manufacturer produces three brands of cars: Big, Tiny, and Boss. Each brand can choose from three types of engines: gasoline, pure electric, and hybrid. If we use traditional inheritance to represent the final car models, we would have a total of 3 abstract classes and 9 final subclasses:
┌───────┐
│ Car │
└───────┘
▲
┌──────────────────┼───────────────────┐
│ │ │
┌───────┐ ┌───────┐ ┌───────┐
│BigCar │ │TinyCar│ │BossCar│
└───────┘ └───────┘ └───────┘
▲ ▲ ▲
│ │ │
│ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
├─│ BigFuelCar │├─│ TinyFuelCar │├─│ BossFuelCar │
│ └───────────────┘│ └───────────────┘│ └───────────────┘
│ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
├─│BigElectricCar │├─│TinyElectricCar│├─│BossElectricCar│
│ └───────────────┘│ └───────────────┘│ └───────────────┘
│ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
└─│ BigHybridCar │└─│ TinyHybridCar │└─│ BossHybridCar │
└───────────────┘ └───────────────┘ └───────────────┘
If we want to add a new brand or a new engine (such as nuclear power), the number of subclasses increases rapidly.
The Bridge pattern aims to avoid the explosion of subclasses that direct inheritance can cause.
Let’s see how the Bridge pattern solves the above problem.
In the Bridge pattern, we first subclass Car
according to brand, but instead of expanding with subclasses for each engine choice, we introduce an abstract "refined" class through composition. Let’s look at the specific implementation.
First, define the abstract class Car
, which references an Engine
:
java
public abstract class Car {
// Reference to Engine:
protected Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public abstract void drive();
}
The definition of Engine
is as follows:
java
public interface Engine {
void start();
}
Next, define some additional operations in an abstract class RefinedCar
:
java
public abstract class RefinedCar extends Car {
public RefinedCar(Engine engine) {
super(engine);
}
public void drive() {
this.engine.start();
System.out.println("Drive " + getBrand() + " car...");
}
public abstract String getBrand();
}
Now, the final different brands inherit from RefinedCar
, such as BossCar
:
java
public class BossCar extends RefinedCar {
public BossCar(Engine engine) {
super(engine);
}
public String getBrand() {
return "Boss";
}
}
For each type of engine, inherit from Engine
, such as HybridEngine
:
java
public class HybridEngine implements Engine {
public void start() {
System.out.println("Start Hybrid Engine...");
}
}
The client can choose a brand and combine it with an engine to get the final Car
:
java
RefinedCar car = new BossCar(new HybridEngine());
car.drive();
The benefit of using the Bridge pattern is that if we want to add a new engine, we only need to derive a new subclass from Engine
. If we want to add a new brand, we just need to derive a new subclass from RefinedCar
. Any subclass of RefinedCar
can freely combine with any type of Engine
, allowing both dimensions—brand and engine—to vary independently.
┌───────────┐
│ Car │
└───────────┘
▲
│
┌───────────┐ ┌─────────┐
│RefinedCar │ ─ ─ ─▶│ Engine │
└───────────┘ └─────────┘
▲ ▲
┌────────┼────────┐ │ ┌──────────────┐
│ │ │ ├─│ FuelEngine │
┌───────┐┌───────┐┌───────┐ │ └──────────────┘
│BigCar ││TinyCar││BossCar│ │ ┌──────────────┐
└───────┘└───────┘└───────┘ ├─│ElectricEngine│
│ └──────────────┘
│ ┌──────────────┐
└─│ HybridEngine │
└──────────────┘
The implementation of the Bridge pattern can be quite complex, and it is less commonly used in practice. However, the design philosophy it provides is worth adopting: do not overuse inheritance but rather prioritize splitting certain components and using composition to extend functionality.
Exercise
Use the Bridge pattern to extend a new brand and a new nuclear-powered engine.
Summary
The Bridge pattern separates an abstract interface from its implementation, allowing the design to be independently extended in two dimensions.