Low-level designCore6 min

Polymorphism

One call, many behaviors: code asks each object to do something, and each class answers in its own way. The caller never checks what kind of object it has, so a new kind needs no change to the caller.

Watch it evolve1 / 8
The problem

A good-night button

The hub turns off lamps, turns down the thermostat and closes blinds.

80
Start
Stage 1
Final

1 · The situation

One button for bedtime

A smart-home hub has a "good night" button: lights off, heating down to 17, blinds closed. Each device is a subclass of Device, and the hub's goodNight() checks what kind each device is and calls the right method on it. Python spells it good_night().

The hub checks each device's kindJava
class Hub {
    private final List<Device> devices = new ArrayList<>();

    void add(Device d) {
        devices.add(d);
    }

    // Lights off, heating down, blinds closed.
    List<String> goodNight() {
        List<String> log = new ArrayList<>();
        for (Device d : devices) {
            if (d instanceof Lamp lamp) {
                lamp.switchOff();
                log.add(lamp.name() + ": off");
            } else if (d instanceof Thermostat t) {
                t.setTarget(17);
                log.add(t.name() + ": set to 17");
            } else if (d instanceof Blind b) {
                b.close();
                log.add(b.name() + ": closed");
            }
        }
        return log;
    }
}
*«abstract»Device− name: String~ name(): StringLamp− on: boolean~ switchOff()Thermostat− targetC: int~ setTarget(int)Blind− open: boolean~ close()Speaker− playing: boolean~ stop()~ isPlaying(): booleanHub− devices: List<Device>~ add(Device)~ goodNight(): List<String>

Drawn from the code: four kinds of Device, and a Hub that holds them and knows every kind by name.

2 · The change

The speaker kept playing

The household adds a kitchen speaker. Speaker is written, it extends Device, and it's added to the hub. Then someone presses good night:

Hall lamp: off
Thermostat: set to 17
Bedroom blind: closed
Kitchen speaker playing: true

The lamp, thermostat and blind did their part. The speaker wasn't in the hub's list of kinds, so nothing told it to stop.

Nothing failed. The chain of checks simply had no branch for the new kind, so it was skipped. Every routine written this way, good night, away from home, morning, needs a new branch for every new device, and each forgotten one fails silently. The practice checks flag the pattern: Hub switches on the Device type.

Checking kinds
2
Each kind answers
1

Places to change to add one kind of device, with one routine. Checking kinds: the new class and the hub. Each kind answers: the new class only.

3 · The fix

Ask every device the same question

Instead of the hub knowing what each kind does at bedtime, let each kind say:

Device gets an abstract goodNight(): every device must say what it does at bedtime, and returns a line for the log.

Java
abstract class Device {
    private final String name;

    protected Device(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }

    // What this device does at bedtime; returns a line for the log.
    abstract String goodNight();
}
*«abstract»Device− name: String~ name(): String~ goodNight(): StringLamp− on: boolean~ goodNight(): StringThermostat− targetC: int~ goodNight(): StringBlind− open: boolean~ goodNight(): StringSpeaker− playing: boolean~ goodNight(): String~ isPlaying(): booleanHub− devices: List<Device>~ add(Device)~ goodNight(): List<String>
Hall lamp: off
Thermostat: set to 17
Bedroom blind: closed
Kitchen speaker: stopped
Kitchen speaker playing: false

All four devices go to bed, the speaker included. A fifth kind would only need its own class.

4 · Dynamic dispatch

One call, chosen at run time

Polymorphism means one interface, many forms: code written against Device works with any kind of device. What makes it work is dynamic dispatch: when the hub calls d.goodNight(), the method that runs is chosen while the program runs, by the class of the object d refers to, not by the declared type of d.

The same line of code in the hub runs Lamp.goodNight() for the lamp and Speaker.goodNight() for the speaker.

*«abstract»Device− name: String~ name(): String~ goodNight(): StringLamp− on: boolean~ goodNight(): StringThermostat− targetC: int~ goodNight(): StringBlind− open: boolean~ goodNight(): StringSpeaker− playing: boolean~ goodNight(): String~ isPlaying(): booleanHub− devices: List<Device>~ add(Device)~ goodNight(): List<String>

After the fix: the hub depends only on Device, and every kind overrides goodNight().

5 · Overloading

A cousin, chosen at compile time

Overloading is several methods with the same name and different parameters, such as add(Device d) and add(Device d, String room). It looks similar, but it works differently:

Aspect

Overriding

Overloading

Where

A subclass redefines a parent's method

One class has methods with the same name

Signature

The same parameters

Different parameters

Chosen

At run time, by the object's class

At compile time, by the arguments' declared types

Languages

All four

Java and C++; not Python or JavaScript

The last two rows hide a classic trap. In Java or C++, given describe(Device d) and describe(Lamp l), a call with a variable declared as Device picks the first one, even when the object is a lamp: overloads go by declared types. When behavior must depend on the object's real kind, override a method on the object. Python and JavaScript have no overloading; Python's functools.singledispatch chooses a function by the first argument's type at run time.

6 · When to check the type

A type check isn't always wrong

  • At the edges. Turning outside input, such as JSON, into objects means deciding which class to build, which is a check by nature, done once.
  • Over a closed set you own. When the kinds are fixed and the language can verify a check covers them all, a switch is clear and safe; the page Enums and sealed hierarchies covers that.
  • Inside equality. An equals() method checks that the other object is the same kind.

The warning sign is the same check, in several places, over a set of kinds that grows. That's where a new kind gets missed, as the speaker was.

7 · In practice

Where does a van fit?

In the parking-lot problem, Sam's draft decides where a vehicle fits by checking each vehicle's kind by name, so electric vans would mean editing that code. The checks flag the switch on the vehicle type; let each vehicle say what size of spot it needs.

Fix the parking-lot draft with polymorphism.

Check yourself

4 questions

1. Why did the speaker keep playing?
2. Device d = new Lamp("Hall lamp"); d.goodNight(); Which method runs?
3. In Java, describe(Device d) and describe(Lamp l) both exist. Device d = new Lamp(...); describe(d); Which runs?
4. When is checking an object's type reasonable?

Takeaways

Remember this

  • Polymorphism: code written against a parent type works with every kind, each answering in its own way.
  • Dynamic dispatch picks an overridden method at run time, by the object's class.
  • A chain of type checks must be edited for every new kind, and fails silently when it isn't.
  • Overloading picks a method at compile time, by declared argument types; Python and JavaScript don't have it.
  • Ask objects to act; don't ask what they are and decide for them.