Polymorphism
Problem
Pay with an if for each kind
The company has regular employees, managers with a bonus and part-time interns. Someone made one Employee class with a "role" field, and the pay is worked out like this:
function monthlyPay(e) { if (e.role === "manager") { return e.salary + e.bonus; } else if (e.role === "intern") { return Math.trunc(e.salary / 2); } return e.salary;}It works. But then a new kind of employee comes along: a contractor. You need a new else if here, and in the function for taxes, and in the function for the payslip, and everywhere anyone asked about the role. And every employee carries a bonus field, even though only managers get a bonus.
The code for the manager is scattered all over the program instead of being in the manager.
The idea
Each class decides for itself
With inheritance from the last lesson, Manager and Intern are separate classes. Let each one say what its monthly pay is. The parent has a general version, and the child writes its own version of the same method:
class Employee { // ... #name, #salary, the constructor and the getters
monthlyPay() { return this.#salary; }}
class Manager extends Employee { // ... #bonus and the constructor
monthlyPay() { return this.salary + this.#bonus; }}const ivan = new Employee("Ivan", 2000);const maria = new Manager("Maria", 3000, 500);console.log(ivan.monthlyPay());console.log(maria.monthlyPay());20003500This is called overriding a method. The child writes a method with the same name. For objects of the child class, its version replaces the parent's.
You need nothing more: there is no keyword and no check. Any method can be overridden. JavaScript looks only at the name, not at the parameters. That is handy, but it means nothing stops a mistake in the name or in the parameters. In a moment you will see what happens then.
More precisely, if you are curious
- In JavaScript there is no way to forbid overriding a method.
- TypeScript, JavaScript with types, has an
overridekeyword and checks it before the program starts. Plain JavaScript has no such keyword.
Try it
What changed?
The intern overrides only the method for the monthly pay:
What will the program print?
class Intern extends Employee { monthlyPay() { return Math.trunc(this.salary / 2); }}
const ana = new Intern("Ana", 1600);console.log(ana.monthlyPay());console.log(ana.salary);The intern's monthlyPay() returns half the salary. The salary itself does not change, and salary is inherited from Employee as it is.
The idea
One array for all
A manager is an employee. So employees, managers and interns can sit in one array and be handled in one loop:
const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];for (const e of staff) { console.log(`${e.name}: €${e.monthlyPay()}`);}Ivan: €2000Maria: €3500Ana: €800The loop calls the same method, e.monthlyPay(), and the result is different. The version that runs is the object's, not the one of the variable's type. This is called polymorphism: Greek for "many forms". One call, and each object answers in its own way.
Now a new kind of employee is a new class with its own monthlyPay(). The loop and the rest of the program stay as they are. There is no if on the role and no bonus field in employees who get no bonus.
The variable e has no type, so through it you can write anything you like, e.bonus too. If the object is a manager, it works. If it is a regular employee, e.bonus is just undefined, and the program carries on as if nothing happened. How to ask objects what they can do is in the lesson "What is it really?".
See it
Which method runs
const staff = [ new Employee("Ivan", 2000), new Manager("Maria", 3000, 500), new Intern("Ana", 1600),];for (const e of staff) { console.log(`${e.name}: €${e.monthlyPay()}`);}Step by step, in words
- The array holds objects of three classes. Each slot holds an arrow.
- e points to Ivan, a plain Employee: Employee's monthlyPay runs.
- e points to Maria, a Manager: her own method runs, salary plus bonus.
- e points to Ana, an Intern: the intern gets half the salary.
- The same line, three different methods. The object e points to decides.
The array holds three arrows. Each one points to an object of a different class. On each round e points to the next object, and the call goes to that object's method: first in Employee, then in Manager, then in Intern.
The choice is made while the program runs. On each round JavaScript looks for monthlyPay in the object's class first, then in its parent. Nobody checks ahead of time that every object has such a method. If one does not, the program stops on its round.
Try it
Find the bug
The program is the same, but the intern gets the full salary:
Ivan: €2000Maria: €3500Ana: €1600Here is the Intern class. Which line has the bug?
Pick the line with the bug.
monthlypay, with a small "p". That is a new method, not an override of monthlyPay. So the parent's version is called. JavaScript says nothing: to JavaScript it is just one more method. You catch a slip like this only from the output.The idea
Add to the parent, do not rewrite it
Sometimes the child does not want to replace the method completely, only to add something to it. A manager is described like an employee, plus the bonus. You call the parent's version with super.toString():
toString() { return `${super.toString()}, bonus €${this.#bonus}`;}console.log(String(new Employee("Ivan", 2000)));console.log(String(new Manager("Maria", 3000, 500)));Ivan (€2000)Maria (€3000), bonus €500If Employee changes how it describes itself, the manager follows on its own.
This is also the toString() from the "Classes and objects" module. There you were already overriding a method: the version in Object.
More precisely, if you are curious
- You can call
super.method()from any method of the child, not only from the overriding one.
- JavaScript does not compare the parameters. If you write
monthlyPay(extra)inManager, it is still an override, and the loop that callse.monthlyPay()givesNaNfor the manager:extraisundefined, and a number plusundefinedis not a number. So the overriding method takes the same parameters as the parent's.
Try it
What is missing?
The intern is described like an employee plus the word "intern". What goes in the blank?
class Intern extends Employee {
toString() {
return `${.toString()}, intern`;
}
}super.toString() gives "Ana (€1600)", and the child adds ", intern". With this.toString() instead, the method would call itself, forever, until the program stopped with RangeError: Maximum call stack size exceeded.
Exercise
Each in its own way
Solve the tasks in your editor. Each task has an array of objects of several classes and one loop.
1. Animal shelter. An Animal class with a name and a makeSound() method that prints "some sound". Dog and Cat override it. Put one of each of the three in an array:
Guest: some soundRex: Woof!Mitzi: Meow, meow!2. The student introduces themselves. Student from the last lesson already has a grade. Override introduce() so that it says the grade too. Use super.introduce(); do not copy the line from Person.
I am Georgi, 45 years old.I am Elena, 19 years old.Average grade: 5.53. Party. A Performance class with a title and a start() method. DanceMove, BreakDanceMove (inherits from DanceMove, adds difficulty) and Song override it. A Party class holds an array of performances and has a startParty() method that starts everything in order and at the end says the party has started.
Dancing moonwalkDancing windmill difficulty 8 out of 10Singing 'Bohemian Rhapsody'Starting raffleThe party has started!More tasks
- Contractor. Add a
Contractorto the employees, with hours and a rate per hour. Their monthly pay is hours × rate. Work out the payroll for Ivan, Maria, Ana and Peter (60 hours at €25):
Payroll: €7800- Payslip. Give
Employeeapayslip()method that returns the text "name: €pay" with the monthly pay. Write it only inEmployee. Why does it work correctly for the manager and for the intern too?
Test