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.
A good-night button
The hub turns off lamps, turns down the thermostat and closes blinds.
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().
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;
}
}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.
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.
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();
}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.
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
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.