Абстрактные классы
Abstract-классы не могут быть инстанцированы напрямую. Они служат базовыми классами с абстрактными методами, которые обязаны реализовать наследники.
Зачем нужно
- Определить общий контракт и частичную реализацию для группы классов
- В отличие от interface, abstract class может содержать реализованные методы
- Паттерн Template Method — скелет алгоритма в базовом классе, детали в наследниках
- Гарантия что все наследники реализуют определённые методы
Где используется
- Базовые классы:
BaseRepository,BaseController,BaseService - Template Method: алгоритм с настраиваемыми шагами
- Стратегии и обработчики: общий интерфейс + общий код
- Фреймворки: NestJS Guards, Interceptors; Angular Components
Предпосылки
- Классы — базовые классы TypeScript
- Интерфейсы — интерфейсы для сравнения
Базовый синтаксис
abstract class Shape {
// Абстрактное свойство — обязаны реализовать
abstract name: string;
// Абстрактный метод — обязаны реализовать
abstract area: number;
abstract perimeter: number;
// Обычный метод — с реализацией, наследуется как есть
describe: string {
return `${this.name}: area=${this.area.toFixed(2)}, perimeter=${this.perimeter.toFixed(2)}`;
}
}
// Нельзя создать экземпляр абстрактного класса
// const shape = new Shape(); // Ошибка! Cannot create an instance of an abstract class
class Circle extends Shape {
name = "Circle";
constructor(public radius: number) {
super;
}
area: number {
return Math.PI * this.radius ** 2;
}
perimeter: number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
name = "Rectangle";
constructor(public width: number, public height: number) {
super;
}
area: number {
return this.width * this.height;
}
perimeter: number {
return 2 * (this.width + this.height);
}
}
const circle = new Circle(5);
console.log(circle.describe);
// "Circle: area=78.54, perimeter=31.42"
// Полиморфизм: тип — абстрактный класс
const shapes: Shape = [new Circle(5), new Rectangle(3, 4)];
shapes.forEach((s) => console.log(s.describe));
Abstract vs Interface
// Interface — только контракт (что реализовать)
interface Serializable {
serialize: string;
deserialize(data: string): void;
}
// Abstract class — контракт + частичная реализация
abstract class BaseEntity {
abstract tableName: string;
// Реализованный метод — наследники получают бесплатно
toJSON: Record<string, unknown> {
return { ...this };
}
// Абстрактный метод — наследники обязаны реализовать
abstract validate: boolean;
}
class User extends BaseEntity {
tableName = "users";
constructor(public name: string, public email: string) {
super;
}
validate: boolean {
return this.name.length > 0 && this.email.includes("@");
}
// toJSON — унаследован, не нужно писать
}
| Критерий | Interface | Abstract Class |
|---|---|---|
| Реализованные методы | Нет | Да |
| Свойства с значениями | Нет | Да |
| Множественное наследование | Да (implements) | Нет (один extends) |
| Конструктор | Нет | Да |
| Runtime-существование | Нет | Да |
| Declaration merging | Да | Нет |
Template Method Pattern
abstract class DataProcessor<T> {
// Шаблонный метод — скелет алгоритма
async process: Promise<void> {
const rawData = await this.fetchData;
const validData = this.validate(rawData);
const transformed = this.transform(validData);
await this.save(transformed);
this.notify;
}
// Абстрактные шаги — реализуют наследники
protected abstract fetchData: Promise<T>;
protected abstract validate(data: T): T;
protected abstract transform(data: T): T;
protected abstract save(data: T): Promise<void>;
// Хук с дефолтной реализацией — можно переопределить
protected notify: void {
console.log("Processing complete");
}
}
class UserImporter extends DataProcessor<User> {
protected async fetchData: Promise<User> {
const res = await fetch("/api/users");
return res.json();
}
protected validate(data: User): User {
return data.filter((u) => u.email.includes("@"));
}
protected transform(data: User): User {
return data.map((u) => ({ ...u, name: u.name.trim() }));
}
protected async save(data: User): Promise<void> {
await fetch("/api/users/bulk", {
method: "POST",
body: JSON.stringify(data),
});
}
// Переопределяем хук
protected override notify: void {
console.log(`Imported ${this.count} users`);
}
private count = 0;
}
Abstract с модификаторами доступа
abstract class BaseRepository<T extends { id: number }> {
// protected — доступен наследникам
protected items: T = ;
// public abstract — обязательный публичный API
abstract findById(id: number): T | null;
// protected abstract — внутренний метод для наследников
protected abstract validateItem(item: T): boolean;
// Реализованный protected метод
protected generateId: number {
return Date.now();
}
// Публичный реализованный метод
getAll: T {
return [...this.items];
}
create(item: Omit<T, "id">): T {
const newItem = { ...item, id: this.generateId } as T;
if (!this.validateItem(newItem)) {
throw new Error("Validation failed");
}
this.items.push(newItem);
return newItem;
}
}
interface User {
id: number;
name: string;
email: string;
}
class UserRepository extends BaseRepository<User> {
findById(id: number): User | null {
return this.items.find((u) => u.id === id) ?? null;
}
protected validateItem(item: User): boolean {
return item.name.length > 0 && item.email.includes("@");
}
}
Частые ошибки
- Попытка создать экземпляр абстрактного класса — нужно создавать наследника
- Забыть реализовать абстрактный метод — компилятор укажет на ошибку
- Слишком много абстракций — если абстрактный класс не имеет реализованных методов, лучше interface
- Множественное наследование — класс может extends только один класс
// Нельзя:
// class User extends BaseEntity, Serializable {} // Ошибка!
// Можно: extends один класс + implements несколько интерфейсов
class User extends BaseEntity implements Serializable, Validatable {
// ...
}
- Abstract static методы не поддерживаются — static не может быть abstract
Практика
- Создайте абстрактный
Shapeс методамиareaиperimeter— реализуйте Circle, Rectangle, Triangle - Реализуйте Template Method для обработки данных: fetch → validate → transform → save
- Создайте
BaseRepository<T>с абстрактными методами и реализованными CRUD-методами - Сравните: напишите тот же контракт через interface и через abstract class
- Создайте абстрактный
Loggerс реализованнымиinfo/warn/errorи абстрактнымwrite
Связанные темы
- Классы — базовые классы
- Модификаторы доступа — public, private, protected
- Интерфейсы — альтернатива для контрактов
- Декораторы — метаданные для классов