I'll Be Backend
Principles · C++

Polymorphism: subclasses instead of a type field

Let each kind answer for itself, so a new kind adds no new branch.

12 steps4 chapters~4 min at 1.25×#oop#polymorphism#subclasses#type code
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the C++ video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–3

Refactor

Steps 4–8

Payoff

Steps 9–10

Recap

Steps 11–12

The code: before and after

Before

class Rental {
    std::string type;
    int days;
public:
    Rental(const std::string& type, int days)
        : type(type), days(days) {}

    int price() const {
        if (type == "economy") return 30 * days;
        if (type == "suv") return 50 * days + 20;
        throw std::invalid_argument(type);
    }

    int freeKm() const {
        if (type == "economy") return 200 * days;
        if (type == "suv") return 150 * days;
        throw std::invalid_argument(type);
    }
};

void print(const Rental& rental) {
    std::cout << rental.price() << ' ' << rental.freeKm() << '\n';
}

int main() {
    Rental city("economy", 3);
    Rental trip("suv", 3);
    print(city); // 90 600
    print(trip); // 170 450
}

After

class Rental {
protected:
    int days;
public:
    explicit Rental(int days) : days(days) {}
    virtual ~Rental() = default;

    virtual int price() const = 0;

    virtual int freeKm() const = 0;
};

class EconomyRental : public Rental {
public:
    explicit EconomyRental(int days) : Rental(days) {}
    int price() const override { return 30 * days; }
    int freeKm() const override { return 200 * days; }
};

class SuvRental : public Rental {
public:
    explicit SuvRental(int days) : Rental(days) {}
    int price() const override { return 50 * days + 20; }
    int freeKm() const override { return 150 * days; }
};

class VanRental : public Rental {
public:
    explicit VanRental(int days) : Rental(days) {}
    int price() const override { return 70 * days; }
    int freeKm() const override { return 100 * days; }
};

void print(const Rental& rental) {
    std::cout << rental.price() << ' ' << rental.freeKm() << '\n';
}

int main() {
    EconomyRental city(3);
    SuvRental trip(3);
    VanRental move(3);
    print(city); // 90 600
    print(trip); // 170 450
    print(move); // 210 300
}

Transcript, step by step

Problem

Step 1: A car rental desk Before

At a car rental desk, a rental knows its type and how many days it runs. Price and free kilometers each check the type in an if chain to find the rule for that car. Main rents an economy car and an SUV for three days, and print shows the price and free kilometers of each.

Step 2: Vans join the fleet Code smell

The fleet now has vans, at seventy dollars a day with a hundred free kilometers a day. The quick way adds a van branch to the price chain, and then another one to the free kilometers chain. Main rents a van for three days, and the run prints two hundred ten and three hundred.

Step 3: One string, an if chain per method Code smell

A type field, which Fowler calls a type code, is a string that says what kind of thing an object is. So every method has to check that string in an if chain before it can do its job. You can see the type field here, the if chain in price here, and the if chain in free kilometers here. Every new kind of car means opening Rental and editing every if chain in it, so the van's rules end up spread over every method. A deposit method would add a third chain, and every later kind would edit all three.

Refactor

Step 4: Refactor first Back it out

Let's take the vans out again. We'll refactor first, so each kind of car gets its own class, and then vans will be one new class.

Step 5: One class per kind Replace Type Code with Subclasses

First, each kind of car gets a subclass, a class that derives from Rental, and Rental gets a virtual destructor as a base class should. Economy Rental and SUV Rental have only a constructor, which passes its type string and the days up to Rental. Main now creates the subclasses, and both if chains still work unchanged.

Step 6: Each kind sets its price Replace Conditional with Polymorphism

Next, each subclass overrides price with the line from its own branch. Rental's price becomes pure virtual, which makes Rental abstract, and days moves to protected so the subclasses can read it. This is polymorphism, because print asks every rental the same question, and each subclass answers it its own way. Last lesson pulled shared code up into a superclass, and this one pushes the code that differs down into subclasses.

Step 7: Each kind sets its kilometers Replace Conditional with Polymorphism

Free kilometers gets the same move, and each subclass overrides it with the line from its own branch. Rental's free kilometers becomes pure virtual too, so the second chain is gone as well.

Step 8: The type string goes Remove Dead Code

Nothing reads the type field anymore, because no if chain is left to ask it. So the field, its constructor parameter and the two strings passed up are deleted.

Payoff

Step 9: Vans, as one class Add vans

Now vans come back, and this time they are one new class, Van Rental. It overrides price with seventy dollars a day, and free kilometers with a hundred a day. Rental, Economy Rental and SUV Rental didn't change, and main only gains the two van lines.

Step 10: Each kind answers for itself Payoff

The type field used to sit here, and the two if chains used to sit here and here. All the van's rules sit in one class now, so the next kind of car is one new class too. This is the Open Closed Principle, because Rental is open for extension and closed for change. It fits here because the fleet grows by kinds of car, while the questions print asks, price and free kilometers, stay the same. And if a new class leaves out free kilometers, it stays abstract, so C plus plus won't compile the line that creates one.

Recap

Step 11: Where we started, where we ended Before / after

On the left, Rental keeps a type string, and price and free kilometers each check it in an if chain. On the right, Rental holds only the days and declares the two questions as pure virtual functions. Each kind of car, like Economy Rental, answers both questions in its own class.

Step 12: When to reach for it Takeaways

Reach for it when two or more methods check the same type field. Skip it when an object changes kind while it lives, like a rental upgraded mid-trip, because an object can't change its class, so use State or Strategy instead. You'll spot the need by the same switch or if chain on one field, repeated in several methods. The standard exception class works this way, because its what function is virtual, and invalid argument and out of range each answer it their own way.