Интерфейси без interface
Проблем
Заплати и фактури
В края на месеца фирмата плаща две неща: заплатите на служителите и фактурите на доставчиците. Искаш един масив и един цикъл: на кого, колко, и накрая общата сума.
Фактурата не е служител. Да я накараш да наследи Employee е грешно. Изречението „фактурата е служител“ не е вярно, а тя ще получи и име, и заплата, и повишение.
Общ родител Payment с полета и код също не е верният отговор: служителите и фактурите нямат общи данни и общ код. Общо им е само, че могат да бъдат платени.
Идеята
Интерфейс: договор „мога да“
Интерфейсът е списък с методи: какво трябва да може класът, но не и как. JavaScript няма interface, а и няма abstract. Договорът е просто уговорка за имената на методите, записана в коментар:
// Договорът „може да бъде платено“. Всеки платим обект има:// amountDue(): колко се дължи// payee(): на когоclass Invoice { #supplier; #amount;
constructor(supplier, amount) { this.#supplier = supplier; this.#amount = amount; }
amountDue() { return this.#amount; }
payee() { return this.#supplier; }}
const power = new Invoice("Доставчик на ток", 340);console.log(`${power.payee()}: ${power.amountDue()} €`);Доставчик на ток: 340 €Езикът не знае за договора. Няма implements: това е запазена дума, и програма с class Invoice implements Payable не тръгва (SyntaxError: Unexpected strict mode reserved word). Затова договорът се пази от имената: обект, който има amountDue() и payee(), може да бъде платен. Това се казва duck typing: „ако ходи като патица и квака като патица, значи е патица“.
Виж го
Един цикъл за всичко, което се плаща
Служителят също приема договора. Неговата сума е месечната заплата:
class Employee { // ... #name, #salary, конструкторът, monthlyPay()
amountDue() { return this.monthlyPay(); }
payee() { return this.#name; }}Сега и служителите, и фактурите имат amountDue() и payee(). Масивът няма тип и нищо не ги свързва, освен еднаквите имена на методите. Това стига на цикъла.
const payments = [ new Employee("Иван", 2000), new Manager("Мария", 3000, 500), new Invoice("Доставчик на ток", 340), new Invoice("Наемодател", 1800),];let total = 0;for (const p of payments) { console.log(`${p.payee()}: ${p.amountDue()} €`); total += p.amountDue();}console.log(`Общо: ${total} €`);Иван: 2000 €Мария: 3500 €Доставчик на ток: 340 €Наемодател: 1800 €Общо: 7640 €Мениджърът не пише нищо ново. Той наследява от Employee и договора, и amountDue(). А amountDue() вика monthlyPay(), който мениджърът е презаписал. Затова Мария получава 3500 €.
Ами ако в масива попадне обект, който не изпълнява договора? Билетът тук има payee(), но няма amountDue():
class Ticket { payee() { return "Кино"; }}
const payments = [new Invoice("Доставчик на ток", 340), new Ticket()];for (const p of payments) { console.log(`${p.payee()}: ${p.amountDue()} €`);}Масивът го приема без въпроси. Фактурата се плаща, а на билета програмата спира: TypeError: p.amountDue is not a function. Грешката се вижда чак когато цикълът стигне до него, не когато билетът влиза в масива.
const payments = [ new Employee("Иван", 2000), new Manager("Мария", 3000, 500), new Invoice("Доставчик на ток", 340),];let total = 0;for (const p of payments) { total += p.amountDue();}console.log(`Общо: ${total} €`);Стъпка по стъпка, с думи
- Масивът пази три обекта от три класа. Общото им е само методът amountDue(), и стрелките стигат само до него.
- total започва от 0.
- p сочи служителя: amountDue() връща заплатата, 2000.
- Мениджърът е служител, затова и той е платим. Заплата плюс бонус: 3500.
- Фактурата не е служител, но изпълнява същия договор: 340.
- Цикълът не знае класовете. Знае само, че всеки може да бъде платен.
Пробвай
Каква е сумата?
Какво ще отпечата програмата? Класовете са от горната карта.
const p = new Manager("Мария", 3000, 500);console.log(p.amountDue());Manager наследява amountDue() от Employee, затова и мениджърът изпълнява договора, без да го пише. Извикването отива в презаписания метод на мениджъра: 3000 + 500.
Идеята
Един родител, много договори
Счетоводството иска фактурите и в CSV формат: по един ред „доставчик;сума“. Това е втори договор. В JavaScript няма какво да се изброява: фактурата просто пише и неговия метод.
// Вторият договор: toCsv() връща ред „доставчик;сума“.
class Invoice { // ... както преди
toCsv() { return `${this.#supplier};${this.#amount}`; }}const forAccounting = [new Invoice("Доставчик на ток", 340), new Invoice("Наемодател", 1800)];for (const e of forAccounting) { console.log(e.toCsv());}Доставчик на ток;340Наемодател;1800extends приема само един родител, но договорите не са родители. Мениджърът си остава extends Employee и само дописва toCsv(). Така клас с вече зает родител пак може да влезе в общ масив с напълно различни класове.
Пробвай
Намери грешката
payee() работи, но power.amountDue() спира програмата. На кой ред е грешката?Избери реда с грешката.
amountdue с малко d. За JavaScript това е друг метод, а amountDue го няма. Никой не проверява договора, затова грешката излиза чак при извикването: TypeError: power.amountDue is not a function.Пробвай
Кое липсва?
class Invoice {
// ...
() {
return this.#amount;
}
}amountDue, точно така, с главно D. В JavaScript нищо друго не свързва класа с договора, освен името на метода.Идеята
Абстрактен клас или интерфейс?
В JavaScript нито едното не е част от езика. Абстрактният родител е клас, който спира с throw. Договорът е само имената на методите. Разликата е в това какво казват:
| Абстрактен клас | Договор | |
|---|---|---|
| Казва | „X е Y“ | „X може да“ |
| Как се пише | extends родител, който хвърля грешка |
нищо: само методи с тези имена |
| Колко на клас | един | колкото искаш |
| Полета и конструктор | да | не |
| Общ код | да | не |
- Абстрактен клас, когато наследниците са един вид и имат общ код и данни: фигурите имат име и описание, служителите имат име и заплата.
- Договор, когато класовете са различни, а им трябва обща способност: служител и фактура „могат да бъдат платени“, птица и самолет „могат да летят“.
Employee е родител на мениджъра и стажанта и същевременно изпълнява договора за плащане.По-точно, за любопитните
- В Java и C# интерфейсът може да има метод по подразбиране, с тяло. В JavaScript договорът го няма като нещо в кода, затова няма къде да стои такъв метод. Общото поведение се слага в обект и се копира в класовете с
Object.assign. Внимание: копирането замества метод със същото име, ако класът вече има свой.
const gliding = { glide() { console.log("Плъзга се във въздуха"); },};
class Bird { fly() { console.log("Птицата маха с крила"); }}
class Plane { fly() { console.log("Самолетът излита"); }}
Object.assign(Bird.prototype, gliding);Object.assign(Plane.prototype, gliding);
const flyers = [new Bird(), new Plane()];for (const f of flyers) { f.fly(); f.glide();}Птицата маха с крилаПлъзга се във въздухаСамолетът излитаПлъзга се във въздуха- Ако искаш да провериш договора преди да викаш метода, питаш дали обектът има такава функция:
typeof p.amountDue === "function". Повече в урока „Какъв е всъщност?“.
Упражнение
Договори
Реши задачите в редактора си. В JavaScript „интерфейсът“ във всяка задача е договор в коментар: класовете просто имат методите с тези имена.
1. Клаксон. Интерфейс Honkable с метод honk(). SimpleCar от модул „Класове“ го реализира.
Бип-бип!2. Подробности. Интерфейс Printable с метод printDetails(). Реализирай го в Person, а Student (наследник на Person) да го презапише и да добави успеха. Сложи двамата в един масив.
Име: Георги, възраст: 45Име: Елена, възраст: 19Среден успех: 5.53. Танц. Интерфейс Danceable с метод dance(). DanceMove (име и продължителност) го реализира.
Танцувам салса 30 секунди4. Диджей. Клас Song със заглавие. Интерфейс Playable с метод play(). DJ го реализира и има метод playSong(song), който запомня песента и я пуска. Ако още няма песен, play() казва това.
Няма избрана песенПускам 'Bohemian Rhapsody'5. Езерото. Интерфейси Swimmable с swim() и Flyable с fly(). Fish плува. Duck и плува, и лети.
Рибата плува.Патицата плува.Патицата лети.Още задачи
- Отстъпка. Интерфейс Discountable с метод, който връща цената след отстъпка. Реализирай го в
Product(10% отстъпка) и вSubscription(25% отстъпка за студенти). Сметни сумата на количка с продукт за 50 € и студентски абонамент за 12 €: 45 + 9 = 54 €. - CSV за всички. Накарай и
Employeeда реализира договора за CSV, с ред „име;месечна заплата“. Сложи служители и фактури в един масив.
Тест