← Курс/Абстрактные классы и методы#161 из 257+25 XP

Абстрактные классы и методы

Что такое абстрактный класс

Абстрактный класс — это класс, от которого нельзя создать экземпляр напрямую. Он служит шаблоном, определяя общую структуру для своих подклассов.

abstract class Animal {
  abstract makeSound(): string  // абстрактный метод — без тела

  // Обычный метод с реализацией — наследуется
  describe(): string {
    return `Я животное, издаю звук: ${this.makeSound()}`
  }
}

// const a = new Animal()  // Ошибка: Cannot create an instance of an abstract class

class Dog extends Animal {
  makeSound(): string {   // ОБЯЗАТЕЛЬНО реализовать
    return 'Гав!'
  }
}

class Cat extends Animal {
  makeSound(): string {
    return 'Мяу!'
  }
}

const dog = new Dog()
console.log(dog.describe())  // 'Я животное, издаю звук: Гав!'

Абстрактные свойства

Абстрактными могут быть не только методы, но и свойства:

abstract class Shape {
  abstract readonly name: string    // абстрактное свойство
  abstract area(): number           // абстрактный метод
  abstract perimeter(): number

  // Готовый метод — общая логика для всех фигур
  toString(): string {
    return `${this.name}: площадь=${this.area().toFixed(2)}`
  }
}

class Circle extends Shape {
  readonly name = 'Круг'
  constructor(private radius: number) { super() }
  area() { return Math.PI * this.radius ** 2 }
  perimeter() { return 2 * Math.PI * this.radius }
}

Abstract vs Interface: когда что выбирать

| Критерий | abstract class | interface |

|---|---|---|

| Реализация методов | Да | Нет (только сигнатуры) |

| Поля с состоянием | Да | Нет |

| Конструктор | Да | Нет |

| Наследование | extends (один) | implements (несколько) |

| Компилируется в JS | Да (остаётся классом) | Нет (стирается) |

| Модификаторы доступа | Да | Нет |

// Interface — только форма данных/контракт
interface Serializable {
  serialize(): string
  deserialize(data: string): this
}

// Abstract class — общая логика + обязательные точки расширения
abstract class BaseRepository<T> {
  protected items: T[] = []

  findAll(): T[] { return [...this.items] }

  findById(id: number): T | undefined {
    return this.items.find((item: any) => item.id === id)
  }

  // Подкласс обязан реализовать эти методы
  abstract validate(item: T): boolean
  abstract create(data: Partial<T>): T
}

Когда использовать abstract class

Используйте абстрактный класс, когда:

1. **Нужна общая логика** — часть методов одинакова для всех подклассов

2. **Есть состояние** — подклассы разделяют общие поля

3. **Шаблонный метод** — алгоритм зафиксирован, детали варьируются

abstract class DataProcessor {
  // Шаблонный метод — алгоритм зафиксирован
  process(data: string[]): string[] {
    const filtered = this.filter(data)   // шаг 1 — переопределяется
    const mapped   = this.transform(filtered)  // шаг 2 — переопределяется
    return this.sort(mapped)             // шаг 3 — общая реализация
  }

  abstract filter(data: string[]): string[]
  abstract transform(data: string[]): string[]

  protected sort(data: string[]): string[] {
    return [...data].sort()
  }
}

**Правило**: если вам нужно описать лишь форму объекта — используйте interface. Если нужна общая реализация с обязательными точками расширения — abstract class.

Примеры

Шаблонный метод через абстрактный класс: иерархия логгеров с общим форматированием

// В TypeScript это было бы abstract class Logger
// В JS эмулируем: конструктор бросает ошибку если вызван напрямую

class Logger {
  constructor(level = 'INFO') {
    if (new.target === Logger) {
      throw new Error('Logger — абстрактный класс')
    }
    this.level = level
  }

  // "Абстрактные" методы — подкласс обязан реализовать
  formatMessage(message) {
    throw new Error(`${this.constructor.name} не реализовал formatMessage()`)
  }

  writeOutput(text) {
    throw new Error(`${this.constructor.name} не реализовал writeOutput()`)
  }

  // Шаблонный метод — алгоритм зафиксирован
  log(message) {
    const timestamp = new Date().toISOString().substring(11, 19)
    const formatted = this.formatMessage(message)
    const output = `[${timestamp}] [${this.level}] ${formatted}`
    this.writeOutput(output)
    return output
  }

  warn(message) {
    const original = this.level
    this.level = 'WARN'
    const result = this.log(message)
    this.level = original
    return result
  }

  error(message) {
    const original = this.level
    this.level = 'ERROR'
    const result = this.log(message)
    this.level = original
    return result
  }
}

// ConsoleLogger — выводит в консоль с цветами (эмуляция через префиксы)
class ConsoleLogger extends Logger {
  constructor() { super('INFO') }

  formatMessage(message) {
    return String(message)
  }

  writeOutput(text) {
    console.log(text)
  }
}

// PrefixLogger — добавляет префикс к каждому сообщению
class PrefixLogger extends Logger {
  constructor(prefix) {
    super('INFO')
    this.prefix = prefix
  }

  formatMessage(message) {
    return `[${this.prefix}] ${message}`
  }

  writeOutput(text) {
    console.log(text)
  }
}

// BufferLogger — накапливает сообщения в буфер
class BufferLogger extends Logger {
  constructor() {
    super('INFO')
    this.buffer = []
  }

  formatMessage(message) {
    return String(message)
  }

  writeOutput(text) {
    this.buffer.push(text)
  }

  getBuffer() {
    return [...this.buffer]
  }

  flush() {
    const lines = this.buffer.join('\n')
    this.buffer = []
    return lines
  }
}

// --- Демонстрация ---

console.log('=== ConsoleLogger ===')
const logger = new ConsoleLogger()
logger.log('Приложение запущено')
logger.warn('Память заканчивается')
logger.error('Соединение с БД потеряно')

console.log('\n=== PrefixLogger ===')
const dbLogger = new PrefixLogger('DB')
dbLogger.log('Подключение установлено')
dbLogger.error('Запрос завершился с ошибкой')

console.log('\n=== BufferLogger ===')
const buf = new BufferLogger()
buf.log('Событие 1')
buf.log('Событие 2')
buf.warn('Предупреждение')
console.log('Буфер накопил:', buf.getBuffer().length, 'сообщений')
console.log('Содержимое буфера:')
console.log(buf.flush())

console.log('\n=== Попытка создать Logger напрямую ===')
try {
  new Logger()
} catch (e) {
  console.log('Ошибка:', e.message)
}