Към съдържанието
wannacode

Полиморфизъм

Проблем

Заплата с if по вид

Във фирмата има служители, мениджъри с бонус и стажанти на половин работен ден. Някой е направил един клас Employee с поле „роля“, а заплатата се смята така:

function monthlyPay(e) {
if (e.role === "мениджър") {
return e.salary + e.bonus;
} else if (e.role === "стажант") {
return Math.trunc(e.salary / 2);
}
return e.salary;
}

Работи. Но после идва нов вид служител, на хонорар. Трябва нов else if тук, и в функцията за данъците, и в функцията за фиша, и навсякъде, където някой е питал за ролята. А всеки служител носи поле за бонус, макар че само мениджърите имат бонус.

Кодът за мениджъра е разпилян из цялата програма, вместо да е в мениджъра.

Идеята

Всеки клас казва сам

С наследяването от миналия урок Manager и Intern са отделни класове. Нека всеки сам каже каква е месечната му заплата. Родителят има обща версия, а наследникът пише своя версия на същия метод:

class Employee {
// ... #name, #salary, конструкторът и getter-ите
monthlyPay() {
return this.#salary;
}
}
class Manager extends Employee {
// ... #bonus и конструкторът
monthlyPay() {
return this.salary + this.#bonus;
}
}
const ivan = new Employee("Иван", 2000);
const maria = new Manager("Мария", 3000, 500);
console.log(ivan.monthlyPay());
console.log(maria.monthlyPay());
Изход
2000
3500

Това се казва презаписване на метод (override). Наследникът пише метод със същото име. За обектите от наследника неговата версия заменя тази на родителя.

Не трябва нищо повече: няма ключова дума и няма проверка. Всеки метод може да бъде презаписан. JavaScript гледа само името, не и параметрите. Това е удобно, но значи, че грешка в името или в параметрите не спира никой. След малко ще видиш какво става тогава.

По-точно, за любопитните
  • В JavaScript няма начин да забраниш презаписването на метод.
  • TypeScript, JavaScript с типове, има ключова дума override и я проверява, преди програмата да тръгне. В самия JavaScript такава няма.

Пробвай

Какво се смени?

Стажантът презаписва само метода за месечната заплата:

Какво ще отпечата програмата?

class Intern extends Employee {
monthlyPay() {
return Math.trunc(this.salary / 2);
}
}
const ana = new Intern("Ана", 1600);
console.log(ana.monthlyPay());
console.log(ana.salary);
Отговор

Идеята

Един масив за всички

Мениджърът е служител. Затова служители, мениджъри и стажанти могат да са в един масив и да се обработят в един цикъл:

const staff = [
new Employee("Иван", 2000),
new Manager("Мария", 3000, 500),
new Intern("Ана", 1600),
];
for (const e of staff) {
console.log(`${e.name}: ${e.monthlyPay()} €`);
}
Изход
Иван: 2000 €
Мария: 3500 €
Ана: 800 €

Цикълът вика един и същ метод, e.monthlyPay(), а резултатът е различен. Изпълнява се версията на обекта, а не на типа на променливата. Това се казва полиморфизъм: на гръцки „много форми“. Едно извикване, а всеки обект отговаря по своя начин.

Сега новият вид служител е нов клас с негов monthlyPay(). Цикълът и останалата програма не се пипат. Няма if по роля, няма поле за бонус в служителите без бонус.

Променливата e няма тип, затова през нея можеш да напишеш каквото искаш, и e.bonus. Ако обектът е мениджър, работи. Ако е обикновен служител, e.bonus е просто undefined, а програмата продължава, все едно нищо не е станало. Как се питат обектите какво могат, ще видим в урока „Какъв е всъщност?“.

Виж го

Кой метод се изпълнява

const staff = [
new Employee("Иван", 2000),
new Manager("Мария", 3000, 500),
new Intern("Ана", 1600),
];
for (const e of staff) {
console.log(`${e.name}: ${e.monthlyPay()} €`);
}
Стъпка по стъпка, с думи
  1. В масива има обекти от три класа. Всяко място пази стрелка.
  2. e сочи Иван, обикновен Employee: изпълнява се monthlyPay на Employee.
  3. e сочи Мария, Manager: изпълнява се нейният метод, заплата плюс бонус.
  4. e сочи Ана, Intern: стажантът взима половината от заплатата.
  5. Един и същ ред, три различни метода. Решава обектът, към който сочи e.

Масивът пази три стрелки. Всяка сочи обект от различен клас. При всеки кръг e сочи следващия обект, а извикването отива в метода на този обект: първо в Employee, после в Manager, после в Intern.

Изборът става, докато програмата работи. На всеки кръг JavaScript търси monthlyPay първо в класа на обекта, после в родителя му. Никой не проверява предварително, че всеки обект има такъв метод. Ако някой няма, програмата спира на неговия кръг.

Пробвай

Намери грешката

Програмата е същата, но стажантът получава цялата заплата:

Изход
Иван: 2000 €
Мария: 3500 €
Ана: 1600 €

Ето класа Intern. На кой ред е грешката?

Избери реда с грешката.

Идеята

Допълни родителя, не го преписвай

Понякога наследникът не иска да замени метода изцяло, а да добави нещо към него. Мениджърът се описва като служител, плюс бонуса. Версията на родителя се вика с super.toString():

toString() {
return `${super.toString()}, бонус ${this.#bonus} €`;
}
console.log(String(new Employee("Иван", 2000)));
console.log(String(new Manager("Мария", 3000, 500)));
Изход
Иван (2000 €)
Мария (3000 €), бонус 500 €

Ако Employee смени как се описва, мениджърът ще го последва сам.

Това е и toString() от модул „Класове“. Там вече презаписваше метод: версията на Object.

По-точно, за любопитните
  • super.method() може да се извика от всеки метод на наследника, не само от презаписания.
  • JavaScript не сверява параметрите. Ако в Manager напишеш monthlyPay(extra), това пак е презаписване, а цикълът, който вика e.monthlyPay(), дава за мениджъра NaN: extra е undefined, а число плюс undefined не е число. Затова презаписаният метод приема същите параметри като на родителя.

Пробвай

Кое липсва?

Стажантът се описва като служител и думата „стажант“. Какво трябва да пише на празното място?

class Intern extends Employee {
  toString() {
    return `${.toString()}, стажант`;
  }
}

Упражнение

Всеки по своему

Реши задачите в редактора си. Във всяка задача има масив с обекти от няколко класа и един цикъл.

1. Приют. Клас Animal с име и метод makeSound(), който печата „някакъв звук“. Dog и Cat го презаписват. Сложи по едно от трите в масив:

Изход
Гост: някакъв звук
Рекс: Бау!
Мици: Мяу, мяу!

2. Студентът се представя. Student от миналия урок вече има успех. Презапиши introduce(), така че да казва и успеха. Използвай super.introduce(), не преписвай реда на Person.

Изход
Аз съм Георги, на 45 години.
Аз съм Елена, на 19 години.
Среден успех: 5.5

3. Купон. Клас Performance със заглавие и метод start(). DanceMove, BreakDanceMove (наследява DanceMove, добавя сложност) и Song го презаписват. Клас Party пази масив с изпълнения и има метод startParty(), който стартира всичко поред и накрая казва, че купонът е започнал.

Изход
Танцуваме moonwalk
Танцуваме windmill
сложност 8 от 10
Пеем 'Bohemian Rhapsody'
Започва томбола
Купонът започна!

Още задачи

  • Хонорар. Добави към служителите Contractor с часове и ставка на час. Месечната му заплата е часове × ставка. Сметни фонда на заплатите за Иван, Мария, Ана и Петър (60 часа по 25 €):
Изход
Фонд работна заплата: 7800 €
  • Фиш. Добави на Employee метод payslip(), който връща текст „име: заплата €“ с месечната заплата. Напиши го само в Employee. Защо работи вярно и за мениджъра, и за стажанта?

Тест

Провери се

1. const e = new Manager("Мария", 3000, 500); Кой monthlyPay се изпълнява при e.monthlyPay()?
2. const staff = [new Employee("Иван", 2000), new Manager("Мария", 3000, 500)]; Какво печата console.log(staff[0].bonus)?
3. Как JavaScript разбира, че методът в наследника презаписва този на родителя?
4. Какво прави super.toString() в toString() на наследника?
5. В Manager е monthlyPay(extra), а в Employee е без параметър. Цикълът вика e.monthlyPay() за всеки служител. Какво печата за Мария?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·