Composition over inheritance: end the subclass explosion
Build each rental from parts, so a new car is one small class.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–11Payoff
Steps 12–13Recap
Steps 14–15The code: before and after
Before
class Rental {
protected:
int days;
public:
explicit Rental(int days) : days(days) {}
virtual ~Rental() = default;
virtual int price() const = 0;
};
class EconomyRental : public Rental {
public:
explicit EconomyRental(int days) : Rental(days) {}
int price() const override { return 30 * days; }
};
class SuvRental : public Rental {
public:
explicit SuvRental(int days) : Rental(days) {}
int price() const override { return 50 * days + 20; }
};
class InsuredEconomy : public EconomyRental {
public:
explicit InsuredEconomy(int days) : EconomyRental(days) {}
int price() const override {
return EconomyRental::price() + 15 * days;
}
};
class InsuredSuv : public SuvRental {
public:
explicit InsuredSuv(int days) : SuvRental(days) {}
int price() const override {
return SuvRental::price() + 15 * days;
}
};
int main() {
EconomyRental city(3);
InsuredSuv trip(3);
std::cout << city.price() << ' ' << trip.price() << '\n'; // 90 215
}After
struct Car {
virtual ~Car() = default;
virtual int price(int days) const = 0;
};
struct EconomyCar : Car {
int price(int days) const override { return 30 * days; }
};
struct SuvCar : Car {
int price(int days) const override { return 50 * days + 20; }
};
struct VanCar : Car {
int price(int days) const override { return 70 * days; }
};
struct Cover {
virtual ~Cover() = default;
virtual int price(int days) const = 0;
};
struct Insurance : Cover {
int price(int days) const override { return 15 * days; }
};
struct NoCover : Cover {
int price(int) const override { return 0; }
};
class Rental {
const Car& car;
const Cover& cover;
int days;
public:
Rental(const Car& car, const Cover& cover, int days)
: car(car), cover(cover), days(days) {}
int price() const { return car.price(days) + cover.price(days); }
};
int main() {
EconomyCar economy;
SuvCar suv;
VanCar van;
NoCover none;
Insurance insurance;
Rental city(economy, none, 3);
Rental trip(suv, insurance, 3);
Rental move(van, insurance, 3);
std::cout << city.price() << ' ' << trip.price() << ' '
<< move.price() << '\n'; // 90 215 255
}Transcript, step by step
Problem
Step 1: A car rental desk Before
At the car rental desk from the last lesson, a rental knows its days, and each subclass sets the price. Economy Rental and SUV Rental each price one car kind, and insurance adds fifteen dollars a day on top. Insurance came after the car classes, so deriving from each one added it without touching them, their callers, or an if inside every price. Main rents an economy car and an insured SUV for three days, and prints ninety and two hundred fifteen.
Step 2: Vans join the fleet Code smell
The fleet now has vans, at seventy dollars a day, and customers can insure them too. The quick way adds Van Rental, and then Insured Van, which derives from it and adds the same fifteen dollars a day. Main rents an insured van for three days, and the run prints two hundred fifty five as the third price.
Step 3: A subclass for every pair Code smell
This is a subclass explosion, when a class varies in two independent ways and inheritance needs one subclass for every pair. Here the two ways are the car kind and the cover, so every car kind needs a second class for insurance. You can see the insurance rule here, here, and here. A subclass per kind worked last lesson, because rentals varied in only one way, the car, but a second way multiplies the classes. One van needed two classes, and the insurance rule now lives in three places, so changing the insurance price means three edits.
Refactor
Step 4: Refactor first Back it out
With vans, three car kinds times two choices of cover make six rental classes, and a premium cover would make it nine. So let's take the vans out again. We'll refactor first, so car and cover become separate parts, and then vans will need one new class.
Step 5: Insurance gets a class Extract Class
First, the insurance rule moves into a struct of its own, Insurance, with a price for a number of days. Both insured subclasses now ask a temporary Insurance for its price, instead of adding fifteen a day themselves. The rule lives in one place now, and the run still prints ninety and two hundred fifteen.
Step 6: Name the cover Extract Interface
Next, an interface named Cover gets one pure virtual method, price for a number of days. Insurance derives from Cover and marks its price as an override, so the compiler checks that the signature matches. We need the interface because the rental will soon hold a cover of any kind, whatever it charges.
Step 7: A cover for no cover Introduce Null Object
A rental without insurance will still need a cover, so No Cover derives from Cover and returns zero. This is a Null Object, an object that stands in for nothing, so an uninsured rental will need no null check and no if.
Step 8: The rental holds a cover Replace Subclass with Delegate
Now Rental gets a cover field, a reference to a Cover that comes in through its constructor. Its price isn't virtual anymore, it's the car's price plus the cover's price for the same days. This is composition, where the rental holds a cover as a field and asks it for its part of the price. The cover is the rental's delegate, the object it hands that part of the work to. Economy Rental and SUV Rental override car price instead, and both insured subclasses are deleted. Main owns one No Cover and one Insurance, and they outlive both rentals, so each rental can safely hold a reference.
Step 9: Each car gets a class Extract Class
The car kind gets the same treatment, so each car's price moves into its own struct, Economy Car and SUV Car. Economy Rental and SUV Rental now ask a temporary car object for the price inside car price.
Step 10: Name the car Extract Interface
Both car structs answer the same question, so an interface named Car declares it. Economy Car and SUV Car derive from it and override price, the same shape Cover has.
Step 11: The rental holds a car Replace Subclass with Delegate
Last, Rental holds a car as well, and its price adds the car's price and the cover's price. Nothing derives from Rental anymore, so it drops the virtual destructor and becomes a plain class, with the car as its second delegate. Economy Rental, SUV Rental and car price are deleted, and main builds each rental from a car, a cover and the days. The run still prints ninety and two hundred fifteen, so nothing changed for the desk's customers.
Payoff
Step 12: Vans, as one class Add vans
Now vans come back, and this time they are one new struct, Van Car, at seventy dollars a day. Main rents a van with the same Insurance object the SUV uses, and the run prints two hundred fifty five. Rental, both interfaces, the other two cars and both covers didn't change, and no insured van class exists.
Step 13: Parts add up Composition over inheritance
The three copies of the insurance rule are now one, here in Insurance. A premium cover would be one more Cover struct, and every car kind could use it. The Gang of Four put it as favor object composition over class inheritance, and it fits here because rentals vary in two independent ways. As fields, each way grows on its own, so three cars plus two covers make five small structs, where inheritance needed six rental subclasses.
Recap
Step 14: Where we started, where we ended Before / after
On the left, every pair of car kind and cover had its own subclass, with the insurance rule copied into each insured one. On the right, Rental holds a car and a cover, and adds their two prices. Each car kind, like SUV Car, and each cover, like Insurance, is one small struct of its own.
Step 15: When to reach for it Takeaways
Reach for it when a class varies in two or more independent ways, like car kind and cover. Skip it when there's only one way to vary, like last lesson's rentals, because nothing multiplies and a delegate only adds an object and a call. You'll spot the need by class names that join words from two lists, like Insured SUV, and the same override copied across sibling subclasses. The standard stack holds whatever container you give it, so there's no Vector Stack subclass.