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

Абстрактни класове без abstract

Проблем

Нула квадратни метра

Пишеш програма за плочки на тераса. Терасата е от три части: правоъгълник 4 × 3 м, кръгла част с радиус 1 м и триъгълник с основа 2 м и височина 3 м. Има родител Shape с area(), който връща 0, а всяка фигура го презаписва.

Колегата, който е писал Triangle, е забравил:

class Triangle extends Shape {
#base;
#height;
constructor(base, height) {
super();
this.#base = base;
this.#height = height;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Плочки: ${total.toFixed(2)} м²`);
Изход
Плочки: 15.14 м²

Трябва 18,14 м². Програмата не дава грешка, а поръчката е с 3 м² по-малко. Нулата на родителя е скрила липсващия метод.

А какво е new Shape()? Фигура без форма. Няма смисъл да съществува такъв обект.

Идеята

Абстрактен клас: обещание, което наследниците изпълняват

Shape не е истинска фигура. Той казва само „всяка фигура има лице“. Затова го направи абстрактен:

class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape е абстрактен: създай конкретна фигура");
}
}
area() {
throw new Error("area() трябва да се напише в наследника");
}
}

JavaScript няма abstract. Родителят сам пази правилото, с две проверки:

  • „Абстрактният“ метод е метод на родителя, който не връща нищо разумно, а хвърля грешка: throw new Error(...) спира програмата и казва защо. Подробно за throw е в модул „Грешки и изключения“.
  • „Абстрактният“ клас проверява в конструктора си new.target: класа, чийто обект се създава. При new Shape() той е Shape, и конструкторът хвърля грешка. При new Circle(1) той е Circle, и проверката пропуска.
  • Затова наследниците викат super() в конструктора си, дори да нямат какво да подадат.

Нищо от това не се проверява предварително. Забравеният Triangle се създава спокойно. Но когато цикълът стигне до неговото area(), програмата спира с „Error: area() трябва да се напише в наследника“, вместо тихо да сметне 3 м² по-малко.

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

Виж го

Триъгълникът изпълнява обещанието

Добави метода в Triangle:

class Triangle extends Shape {
// ... #base, #height и конструкторът
area() {
return this.#base * this.#height / 2;
}
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
let total = 0;
for (const s of terrace) {
total += s.area();
}
console.log(`Плочки: ${total.toFixed(2)} м²`);
Изход
Плочки: 18.14 м²

Цикълът е същият. Масивът пази фигури от три класа, а s.area() вика метода на всяка. Полиморфизмът от миналия урок работи и с абстрактен родител.

Пробвай

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

Програмата спира на един от редовете. Кой?

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

Идеята

Абстрактен, но не празен

Абстрактният клас може да има всичко, което има обикновеният: полета, конструктор, обикновени методи. Нека всяка фигура има име и метод, който я описва:

class Shape {
#name;
constructor(name) {
if (new.target === Shape) {
throw new Error("Shape е абстрактен: създай конкретна фигура");
}
this.#name = name;
}
area() {
throw new Error("area() трябва да се напише в наследника");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} м²`;
}
}

describe() е написан веднъж, в Shape. Той вика area(), който в Shape само хвърля грешка. Не е проблем: describe() винаги се вика върху истинска фигура, а тя има лице.

От Shape не се правят обекти, но конструкторът му е полезен: наследниците го викат.

class Circle extends Shape {
#radius;
constructor(radius) {
super("кръг");
this.#radius = radius;
}
// ... area() както преди
}
const terrace = [new Rectangle(4, 3), new Circle(1), new Triangle(2, 3)];
for (const s of terrace) {
console.log(s.describe());
}
Изход
правоъгълник: 12.00 м²
кръг: 3.14 м²
триъгълник: 3.00 м²

Родителят пише общото (името, описанието), а наследниците пишат различното (лицето).

area() {
throw new Error("area() трябва да се напише в наследника");
}
describe() {
return `${this.#name}: ${this.area().toFixed(2)} м²`;
}
const s = new Circle(1);
console.log(s.describe());
Стъпка по стъпка, с думи
  1. Shape „обещава“ area(): тялото само хвърля грешка. Обект само от Shape не може да има.
  2. Обектът е кръг: частта Circle има свой area(). Тя изпълнява обещанието.
  3. s.describe() е написан в Shape и важи за всяка фигура.
  4. Той вика area(). Обектът е кръг, затова се изпълнява методът на Circle: π · 1 · 1.
  5. describe() сглобява текста. Общото е в родителя, различното в наследника.
  6. Изходът е лицето на кръга, с два знака след запетаята.

Пробвай

Чий метод?

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

// Shape и Circle са от горната карта
const s = new Circle(2);
console.log(s.describe());
Отговор

Пробвай

Кое липсва?

Какво трябва да пише на празното място, за да не може да се създаде обект от Shape?

class Shape {
  constructor() {
    if ( === Shape) {
      throw new Error("Shape е абстрактен: създай конкретна фигура");
    }
  }

  area() {
    throw new Error("area() трябва да се напише в наследника");
  }
}

Упражнение

Фигури и животни

Реши задачите в редактора си.

1. Лице и обиколка. Абстрактен клас Shape с име и два абстрактни метода: area() и perimeter(). Наследници Circle (радиус) и Rectangle (ширина и височина). Сложи кръг с радиус 5 и правоъгълник 4 × 2,5 в масив и отпечатай двете стойности с два знака след точката.

Изход
Кръг: лице 78.54, обиколка 31.42
Правоъгълник: лице 10.00, обиколка 13.00

2. Животните. Абстрактен клас Animal с абстрактен метод sound() и обикновен метод breathe(), който печата „Животното диша.“ Наследници Cat и Dog. Извикай звука на двете в цикъл, после breathe() на първото.

Изход
Мяу, мяу!
Бау, бау!
Животното диша.

Упражнение

Превозни средства и изпълнения

3. Превозни средства. Абстрактен клас Vehicle с абстрактен метод move(). SimpleCar и Bicycle го наследяват. Bicycle има и свой метод pedal().

Изход
Колата потегля
Велосипедът потегля
Въртя педалите
Опитай garage[1].pedal(): работи, защото там има велосипед. А garage[0].pedal()? Защо програмата спира чак на този ред, а не по-рано?

4. Изпълнения. Абстрактен клас Performance със заглавие и абстрактен метод start(). DanceMove и Song го наследяват. Song има и метод sing().

Изход
Започва танцът moonwalk
Започва песента 'Bohemian Rhapsody'
Пея 'Bohemian Rhapsody'

Още задачи

  • Триъгълник. Добави Triangle към задача 1 с трите страни. Обиколката е сумата им, а лицето се смята по формулата на Херон: s = обиколка / 2, лице = √(s(s − a)(s − b)(s − c)). За страни 3, 4 и 5 лицето е 6,00, а обиколката 12,00.
  • Абстрактен служител. Направи Employee от миналия урок абстрактен, с абстрактен monthlyPay(). Добави FullTime (заплатата е цялата) и остави Manager и Intern. Какво печели програмата? Какво губи?

Тест

Провери се

1. Конструкторът на Shape хвърля грешка, ако new.target === Shape. Кое работи?
2. Има ли JavaScript ключова дума abstract?
3. Triangle наследява Shape, но не пише area(). Какво става?
4. Може ли „абстрактен“ клас да има конструктор, полета и обикновени методи?

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