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

Интерфейси без 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} €`);
Стъпка по стъпка, с думи
  1. Масивът пази три обекта от три класа. Общото им е само методът amountDue(), и стрелките стигат само до него.
  2. total започва от 0.
  3. p сочи служителя: amountDue() връща заплатата, 2000.
  4. Мениджърът е служител, затова и той е платим. Заплата плюс бонус: 3500.
  5. Фактурата не е служител, но изпълнява същия договор: 340.
  6. Цикълът не знае класовете. Знае само, че всеки може да бъде платен.

Пробвай

Каква е сумата?

Какво ще отпечата програмата? Класовете са от горната карта.

const p = new Manager("Мария", 3000, 500);
console.log(p.amountDue());
Отговор

Идеята

Един родител, много договори

Счетоводството иска фактурите и в 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
Наемодател;1800

extends приема само един родител, но договорите не са родители. Мениджърът си остава extends Employee и само дописва toCsv(). Така клас с вече зает родител пак може да влезе в общ масив с напълно различни класове.

Пробвай

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

Фактурата се създава и payee() работи, но power.amountDue() спира програмата. На кой ред е грешката?

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

Пробвай

Кое липсва?

class Invoice {
  // ...

  () {
    return this.#amount;
  }
}

Идеята

Абстрактен клас или интерфейс?

В 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.5

3. Танц. Интерфейс 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, с ред „име;месечна заплата“. Сложи служители и фактури в един масив.

Тест

Провери се

1. Как в JavaScript клас казва, че изпълнява договора „може да бъде платено“?
2. В масива payments попада обект без amountDue(). Кога ще разбереш?
3. Класът пише amountdue() вместо amountDue(). Какво става?
4. Колко договора може да изпълнява един клас?
5. Служители и фактури трябва да се плащат в един цикъл. Те нямат общ код. Какво е по-подходящо?

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