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

Abstract-классы не могут быть инстанцированы напрямую. Они служат базовыми классами с абстрактными методами, которые обязаны реализовать наследники.

Зачем нужно

  • Определить общий контракт и частичную реализацию для группы классов
  • В отличие от interface, abstract class может содержать реализованные методы
  • Паттерн Template Method — скелет алгоритма в базовом классе, детали в наследниках
  • Гарантия что все наследники реализуют определённые методы

Где используется

  • Базовые классы: BaseRepository, BaseController, BaseService
  • Template Method: алгоритм с настраиваемыми шагами
  • Стратегии и обработчики: общий интерфейс + общий код
  • Фреймворки: NestJS Guards, Interceptors; Angular Components

Предпосылки

Базовый синтаксис

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("@");
  }
}

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

  1. Попытка создать экземпляр абстрактного класса — нужно создавать наследника
  2. Забыть реализовать абстрактный метод — компилятор укажет на ошибку
  3. Слишком много абстракций — если абстрактный класс не имеет реализованных методов, лучше interface
  4. Множественное наследование — класс может extends только один класс
// Нельзя:
// class User extends BaseEntity, Serializable {} // Ошибка!

// Можно: extends один класс + implements несколько интерфейсов
class User extends BaseEntity implements Serializable, Validatable {
  // ...
}
  1. Abstract static методы не поддерживаются — static не может быть abstract

Практика

  1. Создайте абстрактный Shape с методами area и perimeter — реализуйте Circle, Rectangle, Triangle
  2. Реализуйте Template Method для обработки данных: fetch → validate → transform → save
  3. Создайте BaseRepository<T> с абстрактными методами и реализованными CRUD-методами
  4. Сравните: напишите тот же контракт через interface и через abstract class
  5. Создайте абстрактный Logger с реализованными info/warn/error и абстрактным write

Связанные темы

Ресурсы