$ sudo teach IT
МОДУЛЬ 8 · УРОК 5

Абстрактные классы

Abstract классы и методы — шаблоны, которые нельзя создать напрямую

Цели урока

  • Понять, что такое абстрактные классы и зачем они нужны
  • Освоить ключевое слово abstract для классов и методов
  • Научиться создавать шаблоны с абстрактными методами
  • Понять разницу между абстрактными классами и интерфейсами
  • Разобрать паттерны: Template Method, Strategy через абстракции
  • Изучить типичные ошибки при работе с абстракциями

1. Зачем нужны абстрактные классы

Представьте, что вы создаёте игру с персонажами. У каждого персонажа есть имя и здоровье, и каждый может атаковать. Но способ атаки разный: воин рубит мечом, маг колдует, лучник стреляет.

Возникает вопрос: должен ли базовый класс Character содержать реализацию метода attack()? Нет, потому что он не знает, как именно атакует конкретный персонаж. Но он знает, что attack() должен существовать.

Аналогия:

Абстрактный класс — это как чертёж автомобиля. На чертеже написано: «должны быть колёса, двигатель, руль». Но сам по себе чертёж — это не автомобиль, на нём нельзя ездить. Чтобы ездить, нужно построить конкретный автомобиль (Toyota, BMW) по этому чертежу. Абстрактный класс нельзя создать (new AbstractClass()) — его можно только наследовать.

Абстрактные классы решают две задачи:

  • Общая реализация — абстрактный класс может содержать конкретные методы и свойства, которые наследуются как обычно
  • Обязательный контракт — абстрактные методы заставляют наследников реализовать определённую логику

2. abstract class — базовый синтаксис

Ключевое слово abstract перед class делает класс абстрактным — его нельзя создать через new:

abstract class Shape {
  constructor(public color: string) {}

  // Абстрактный метод — БЕЗ реализации
  // Наследники ОБЯЗАНы его реализовать
  abstract area(): number;

  // Обычный метод — с реализацией
  describe(): string {
    return `${this.color} фигура площадью ${this.area()}`;
  }
}

// ❌ Нельзя создать экземпляр абстрактного класса!
// const s = new Shape("красный"); // ❌ Cannot create an instance of an abstract class

Теперь создаём конкретные наследники, которые обязаны реализовать абстрактные методы:

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

  // ✅ Обязательная реализация абстрактного метода
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(
    color: string,
    public width: number,
    public height: number
  ) {
    super(color);
  }

  area(): number {
    return this.width * this.height;
  }
}
Важно:

Если наследник абстрактного класса не реализует все абстрактные методы, он тоже должен быть абстрактным. TypeScript выдаст ошибку: «Non-abstract class 'X' does not implement inherited abstract member 'method' from class 'Y'».

Пример ошибки, если забыли реализовать абстрактный метод:

// ❌ Ошибка: Triangle не реализовал area()
class Triangle extends Shape {
  // Забыли реализовать area()!
}
// ❌ Non-abstract class 'Triangle' does not implement inherited
// abstract member 'area' from class 'Shape'

// ✅ Либо реализуем, либо делаем Triangle абстрактным:
abstract class Triangle extends Shape {
  // Ок — Triangle тоже абстрактный, area() реализует его наследник
}

3. Абстрактные методы — подробнее

Абстрактный метод — это метод без реализации (без тела {}). Он только объявляет сигнатуру: имя, параметры и возвращаемый тип.

// Абстрактный метод — только сигнатура, без тела
abstract area(): number;

// Обычный метод — сигнатура + тело
describe(): string { /* реализация */ }

Абстрактный метод может принимать параметры:

abstract class Logger {
  // Абстрактные методы — разная сигнатура
  abstract log(message: string): void;
  abstract logError(error: Error): void;
  abstract clear(): void;

  // Обычный метод — использует абстрактные
  logWithTimestamp(msg: string): void {
    this.log(`[${new Date().toISOString()}] ${msg}`);
  }
}

// Конкретная реализация — консольный логгер
class ConsoleLogger extends Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }

  logError(error: Error): void {
    console.error(`[ERROR] ${error.message}`);
  }

  clear(): void {
    console.clear();
  }
}

// Другая реализация — файловый логгер
class FileLogger extends Logger {
  log(message: string): void {
    // Запись в файл — другая реализация!
    console.log(`[FILE] Запись: ${message}`);
  }

  logError(error: Error): void {
    console.log(`[FILE] Ошибка: ${error.message}`);
  }

  clear(): void {
    console.log("Файл очищен");
  }
}

4. Паттерн Template Method

Абстрактные классы идеальны для паттерна Template Method — когда общий алгоритм определён в родителе, а конкретные шаги реализуют наследники:

abstract class DataProcessor {
  // Template Method — общий алгоритм
  process(): void {
    this.readData();
    this.transformData();
    this.saveData();
  }

  private readData(): void {
    console.log("Чтение данных...");
  }

  // Абстрактные шаги — каждый наследник делает по-своему
  abstract transformData(): void;
  abstract saveData(): void;
}

// Конкретный процессор — JSON
class JsonProcessor extends DataProcessor {
  transformData(): void {
    console.log("Трансформация в JSON...");
  }
  saveData(): void {
    console.log("Сохранение в JSON-файл");
  }
}

// Конкретный процессор — CSV
class CsvProcessor extends DataProcessor {
  transformData(): void {
    console.log("Трансформация в CSV...");
  }
  saveData(): void {
    console.log("Сохранение в CSV-файл");
  }
}

// Использование
const json = new JsonProcessor();
json.process();
// "Чтение данных..."
// "Трансформация в JSON..."
// "Сохранение в JSON-файл"

5. Абстрактные классы vs Интерфейсы

И абстрактные классы, и интерфейсы определяют «контракт» для наследников. Но у них есть принципиальные различия:

Абстрактный класс Интерфейс
Реализация методов Может содержать Только сигнатуры
Свойства Может иметь конструктор, свойства Только сигнатуры
Наследование extends (только один) implements (несколько)
Создание объектов Нельзя Нельзя
Когда использовать Есть общая реализация Только контракт
Правило:

Используйте абстрактный класс, когда есть общая логика (конкретные методы, свойства, конструктор). Используйте интерфейс, когда нужен только контракт (сигнатура методов) без реализации. Интерфейс предпочтительнее, когда нужна множественная реализация.

6. Практический пример — Система оплаты

abstract class PaymentMethod {
  protected transactionLog: string[] = [];

  constructor(public name: string) {}

  // Абстрактные методы — каждый способ оплаты реализует по-своему
  abstract pay(amount: number): boolean;
  abstract validate(): boolean;

  // Общий метод — Template Method
  processPayment(amount: number): boolean {
    if (!this.validate()) {
      this.transactionLog.push(`ОШИБКА: валидация не прошла`);
      return false;
    }
    const success = this.pay(amount);
    this.transactionLog.push(
      `${success ? "УСПЕХ" : "ОШИБКА"}: ${this.name} — ${amount} руб.`
    );
    return success;
  }

  getLog(): string[] {
    return [...this.transactionLog];
  }
}

// Банковская карта
class CreditCard extends PaymentMethod {
  constructor(
    name: string,
    public cardNumber: string,
    public balance: number
  ) {
    super(name);
  }

  pay(amount: number): boolean {
    if (amount > this.balance) return false;
    this.balance -= amount;
    return true;
  }

  validate(): boolean {
    return this.cardNumber.length === 16;
  }
}

// Электронный кошелёк
class EWallet extends PaymentMethod {
  constructor(
    name: string,
    public email: string,
    public balance: number
  ) {
    super(name);
  }

  pay(amount: number): boolean {
    if (amount > this.balance) return false;
    this.balance -= amount;
    return true;
  }

  validate(): boolean {
    return this.email.includes("@");
  }
}

Полиморфное использование:

function checkout(method: PaymentMethod, amount: number): void {
  console.log(`Оплата ${method.name} на ${amount} руб.`);
  method.processPayment(amount);
}

const card = new CreditCard("Анна", "1234567890123456", 50000);
const wallet = new EWallet("Борис", "boris@mail.ru", 30000);

checkout(card, 15000); // "УСПЕХ: Анна — 15000 руб."
checkout(wallet, 5000); // "УСПЕХ: Борис — 5000 руб."

7. Частые ошибки

Ошибка 1: Создание экземпляра абстрактного класса

// ❌ Cannot create an instance of an abstract class
abstract class Shape {
  abstract area(): number;
}

// const s = new Shape(); // ❌ Нельзя!

// ✅ Создаём только конкретных наследников
class Circle extends Shape {
  area(): number { return 100; }
}
const c = new Circle(); // ✅

Ошибка 2: Забыли реализовать абстрактный метод

// ❌ Non-abstract class does not implement abstract member
abstract class Animal {
  abstract makeSound(): string;
}

class Dog extends Animal {
  // Забыли реализовать makeSound()!
}

// ✅ Решение: добавить реализацию
class Dog2 extends Animal {
  makeSound(): string { return "Гав!"; }
}

Ошибка 3: Путаница abstract class и interface

// ❌ Интерфейс не может содержать реализацию
// interface Shape { area(): number { return 0; } } // ❌

// ✅ Интерфейс — только сигнатуры
interface Shape {
  area(): number;
}

// ✅ Абстрактный класс — сигнатуры + реализация
abstract class Shape2 {
  abstract area(): number;
  describe(): string { /* реализация */ }
}

Практическое задание

Создайте абстрактный класс Vehicle и два наследника:

  • abstract Vehicle — name, abstract startEngine(), abstract stopEngine(), start() (общий метод)
  • Car extends Vehicle — реализация: startEngine запускает бензиновый мотор
  • ElectricCar extends Vehicle — реализация: startEngine запускает электромотор
  • Попробуйте new Vehicle() — ошибка!
  • Создайте массив Vehicle[] и вызовите start() для каждого

Чек-лист урока

  • abstract class — класс, который нельзя создать через new
  • abstract method — метод без реализации, обязательный для наследников
  • Нельзя new — абстрактный класс создаёт только экземпляры наследников
  • Обязательная реализация — наследник ДОЛЖЕН реализовать все абстрактные методы
  • Общая логика — абстрактный класс может содержать конкретные методы и свойства
  • Template Method — паттерн: общий алгоритм в родителе, шаги — в наследниках
  • vs Interface — абстрактный класс для общей логики, интерфейс для контракта
  • extends для абстрактного — наследование обычное: class X extends AbstractY
  • Частые ошибки — new AbstractClass(), забыли реализовать метод, путаница с интерфейсами

Тест по абстрактным классам

8 вопросов

Абстрактный класс Shape

Premium

Абстрактный класс для базы данных

Premium