Полиморфизъм
Проблем
Заплата с 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());20003500Това се казва презаписване на метод (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);monthlyPay() на стажанта връща половината заплата. Самата заплата не се променя, а salary е наследен от Employee без промяна.
Идеята
Един масив за всички
Мениджърът е служител. Затова служители, мениджъри и стажанти могат да са в един масив и да се обработят в един цикъл:
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()} €`);}Стъпка по стъпка, с думи
- В масива има обекти от три класа. Всяко място пази стрелка.
- e сочи Иван, обикновен Employee: изпълнява се monthlyPay на Employee.
- e сочи Мария, Manager: изпълнява се нейният метод, заплата плюс бонус.
- e сочи Ана, Intern: стажантът взима половината от заплатата.
- Един и същ ред, три различни метода. Решава обектът, към който сочи e.
Масивът пази три стрелки. Всяка сочи обект от различен клас. При всеки кръг e сочи следващия обект, а извикването отива в метода на този обект: първо в Employee, после в Manager, после в Intern.
Изборът става, докато програмата работи. На всеки кръг JavaScript търси monthlyPay първо в класа на обекта, после в родителя му. Никой не проверява предварително, че всеки обект има такъв метод. Ако някой няма, програмата спира на неговия кръг.
Пробвай
Намери грешката
Програмата е същата, но стажантът получава цялата заплата:
Иван: 2000 €Мария: 3500 €Ана: 1600 €Ето класа Intern. На кой ред е грешката?
Избери реда с грешката.
monthlypay, с малко „p“. Това е нов метод, а не презаписване на monthlyPay. Затова се вика версията на родителя. JavaScript не казва нищо: за него това е просто още един метод. Такъв пропуск се хваща само по изхода.Идеята
Допълни родителя, не го преписвай
Понякога наследникът не иска да замени метода изцяло, а да добави нещо към него. Мениджърът се описва като служител, плюс бонуса. Версията на родителя се вика с 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()}, стажант`;
}
}super.toString() дава „Ана (1600 €)“, а наследникът добавя „, стажант“. С this.toString() вместо него методът щеше да вика сам себе си, без край, докато програмата не спре с RangeError: Maximum call stack size exceeded.
Упражнение
Всеки по своему
Реши задачите в редактора си. Във всяка задача има масив с обекти от няколко класа и един цикъл.
1. Приют. Клас Animal с име и метод makeSound(), който печата „някакъв звук“. Dog и Cat го презаписват. Сложи по едно от трите в масив:
Гост: някакъв звукРекс: Бау!Мици: Мяу, мяу!2. Студентът се представя. Student от миналия урок вече има успех. Презапиши introduce(), така че да казва и успеха. Използвай super.introduce(), не преписвай реда на Person.
Аз съм Георги, на 45 години.Аз съм Елена, на 19 години.Среден успех: 5.53. Купон. Клас Performance със заглавие и метод start(). DanceMove, BreakDanceMove (наследява DanceMove, добавя сложност) и Song го презаписват. Клас Party пази масив с изпълнения и има метод startParty(), който стартира всичко поред и накрая казва, че купонът е започнал.
Танцуваме moonwalkТанцуваме windmill сложност 8 от 10Пеем 'Bohemian Rhapsody'Започва томболаКупонът започна!Още задачи
- Хонорар. Добави към служителите
Contractorс часове и ставка на час. Месечната му заплата е часове × ставка. Сметни фонда на заплатите за Иван, Мария, Ана и Петър (60 часа по 25 €):
Фонд работна заплата: 7800 €- Фиш. Добави на
Employeeметодpayslip(), който връща текст „име: заплата €“ с месечната заплата. Напиши го само вEmployee. Защо работи вярно и за мениджъра, и за стажанта?
Тест