Абстрактные классы
Abstract классы и методы — шаблоны, которые нельзя создать напрямую
Цели урока
- Понять, что такое абстрактные классы и зачем они нужны
- Освоить ключевое слово abstract для классов и методов
- Научиться создавать шаблоны с абстрактными методами
- Понять разницу между абстрактными классами и интерфейсами
- Разобрать паттерны: Template Method, Strategy через абстракции
- Изучить типичные ошибки при работе с абстракциями
1. Зачем нужны абстрактные классы
Представьте, что вы создаёте игру с персонажами. У каждого персонажа есть имя и здоровье, и каждый может атаковать. Но способ атаки разный: воин рубит мечом, маг колдует, лучник стреляет.
Возникает вопрос: должен ли базовый класс Character содержать реализацию метода attack()? Нет, потому что он не знает, как именно атакует конкретный персонаж. Но он знает, что attack() должен существовать.
Абстрактный класс — это как чертёж автомобиля. На чертеже написано: «должны быть колёса, двигатель, руль». Но сам по себе чертёж — это не автомобиль, на нём нельзя ездить. Чтобы ездить, нужно построить конкретный автомобиль (Toyota, BMW) по этому чертежу. Абстрактный класс нельзя создать (new AbstractClass()) — его можно только наследовать.
Абстрактные классы решают две задачи:
- Общая реализация — абстрактный класс может содержать конкретные методы и свойства, которые наследуются как обычно
- Обязательный контракт — абстрактные методы заставляют наследников реализовать определённую логику
2. abstract class — базовый синтаксис
Ключевое слово abstract перед class делает класс абстрактным — его нельзя создать через new:
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
Теперь создаём конкретные наследники, которые обязаны реализовать абстрактные методы:
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'».
Пример ошибки, если забыли реализовать абстрактный метод:
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 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 — когда общий алгоритм определён в родителе, а конкретные шаги реализуют наследники:
// 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. Практический пример — Система оплаты
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("@");
}
}
Полиморфное использование:
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: Создание экземпляра абстрактного класса
abstract class Shape {
abstract area(): number;
}
// const s = new Shape(); // ❌ Нельзя!
// ✅ Создаём только конкретных наследников
class Circle extends Shape {
area(): number { return 100; }
}
const c = new Circle(); // ✅
Ошибка 2: Забыли реализовать абстрактный метод
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 вопросов