Форум программистов, компьютерный форум, киберфорум
run.dev
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

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

Запись от run.dev размещена 15.04.2025 в 21:59
Показов 3568 Комментарии 0
Метки typescript

Нажмите на изображение для увеличения
Название: 42b075cb-c4eb-4c25-9e48-a576ec63c790.jpg
Просмотров: 177
Размер:	169.8 Кб
ID:	10597
Разработка современных веб-приложений требует надежных инструментов для структурирования кода. В этом контексте абстрактные классы стали незаменимым элементом объектно-ориентированного программирования. Они представляют собой особый тип классов, от которых нельзя создать экземпляры напрямую — можно только наследовать. По сути, это своеобразные шаблоны, предназначенные для создания более специализированных классов.

Абстрактные классы в TypeScript: основа качественной архитектуры приложений



В TypeScript реализация абстрактных классов обладает рядом уникальных особенностей. В отличие от JavaScript, где концепция абстрактных классов отсутствует на уровне языка, TypeScript вводит ключевое слово abstract, которое явно указывает разработчику и компилятору: "Этот класс нельзя инстанцировать напрямую". При попытке создать экземпляр абстрактного класса компилятор выдаст ошибку — такой подход позволяет предотвратить множество проблем еще на этапе написания кода.

TypeScript
1
2
3
4
5
6
7
8
9
abstract class Vehicle {
  abstract start(): void;
  stop(): void {
    console.log('Остановка транспортного средства');
  }
}
 
// Ошибка: нельзя создать экземпляр абстрактного класса
const vehicle = new Vehicle();
Почему же TypeScript нуждался в абстрактных классах? С ростом сложности веб-приложений стандартных механизмов JavaScript стало недостаточно для эффективной организации кода. Абстрактные классы заполняют важную нишу между интерфейсами (которые только определяют структуру) и обычными классами (с полной реализацией). Эта концепция позволяет разработчикам создавать частичную реализацию, где базовый класс содержит общую логику, а дочерние классы дополняют её своими специфичными деталями. Такой подход способствует соблюдению принципа DRY (Don't Repeat Yourself) и улучшает поддерживаемость кода.

Что делает абстрактные классы в TypeScript особенно ценными — это возможность комбинировать абстрактные методы (без реализации) с конкретными методами (с полной реализацией) в рамках одного класса. Например, абстрактный класс Product может определять общие свойства всех товаров и реализовывать общие методы, но оставлять специфичные методы вроде расчёта налога на усмотрение дочерних классов.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Product {
  name: string;
  price: number;
  
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
  
  getInfo(): string {
    return `${this.name}: $${this.price}`;
  }
  
  abstract calculateTax(): number;
}
Такой механизм позволяет создавать гибкие, расширяемые архитектуры, что критически важно для масштабных проектов, где требования часто меняются, а кодовая база постоянно растёт.

TypeScript vs Script# vs
У кого какой опыт ? - сравнительные достоинства и недостатки.

Перевод C# на TypeScript
Доброго времени суток))) (Извините если не в ту тему) Существует рабочая программы для локального...

VS2012 + typescript 9.1.1
При работе с TypeScript VS2012 виснет или закрывается регулярно, никакой конкретной информации об...

Создать редактор радиосхем для MVC5, используя TypeScript
Ребята!) Нужна инфа как возможно создать редактор,используя typescript (js нежелательно),на mvc 5...


Теоретические основы



Абстрактные классы в TypeScript представляют собой фундаментальную концепцию объектно-ориентированного программирования, которая помогает разработчикам создавать иерархии классов с общим базовым функционалом. Их главная особенность — невозможность прямого создания экземпляров. Абстрактный класс служит каркасом, который определяет структуру и поведение для дочерних классов, требуя от них реализации определённых методов. Ключевое назначение абстрактных классов — создание шаблонов для похожих объектов с общей базовой функциональностью, но с различными способами реализации некоторых операций. Это позволяет устанавливать контракты (аналогично интерфейсам), одновременно предоставляя общую реализацию.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Shape {
  color: string;
  
  constructor(color: string) {
    this.color = color;
  }
  
  // Конкретный метод с реализацией
  describe(): string {
    return `Фигура цвета ${this.color}`;
  }
  
  // Абстрактный метод без реализации
  abstract calculateArea(): number;
}
Концепция абстрактных классов не возникла внезапно. Она эволюционировала в течение десятилетий развития языков программирования. В ранних объектно-ориентированных языках, таких как Simula (считающийся первым ООП-языком), уже существовали примитивные версии этой концепции. Абстрактные классы получили формальное определение в C++ через спецификаторы чисто виртуальных функций. В Java они были явно определены с использованием ключевого слова abstract. В C# абстрактные классы также стали важным компонентом языка. JavaScript долгое время не имел прямой поддержки классов и абстрактных классов, полагаясь вместо этого на прототипное наследование. Только с появлением TypeScript и, в определённой степени, с введением классов в ECMAScript 2015 (ES6), разработчики JavaScript-приложений получили доступ к более привычным ООП-паттернам.

Часто возникает вопрос о различиях между абстрактными классами и интерфейсами, поскольку обе концепции определяют контракты для классов. Основные различия заключаются в следующем:
1. Реализация методов: Абстрактные классы могут содержать как абстрактные (без реализации), так и конкретные методы с реализацией. Интерфейсы содержат только сигнатуры методов без реализации.
2. Множественное наследование: В TypeScript класс может наследовать только от одного класса (абстрактного или конкретного), но может реализовать множество интерфейсов.
3. Свойства: Абстрактные классы могут содержать свойства со значениями, в то время как интерфейсы только объявляют существование свойств без значений по умолчанию.
4. Модификаторы доступа: В абстрактных классах можно использовать модификаторы private, protected и public. Интерфейсы подразумевают только публичные члены.

Синтаксис объявления абстрактного класса в TypeScript довольно прост — используется ключевое слово abstract перед словом class:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Database {
  abstract connect(): boolean;
  abstract disconnect(): void;
  
  executeQuery(query: string): any[] {
    // Общая реализация для всех баз данных
    console.log(`Выполняется запрос: ${query}`);
    return [];
  }
}
 
class MySQLDatabase extends Database {
  connect(): boolean {
    console.log("Подключение к MySQL");
    return true;
  }
  
  disconnect(): void {
    console.log("Отключение от MySQL");
  }
}
В приведённом примере Database — это абстрактный класс с двумя абстрактными методами и одним конкретным. Класс MySQLDatabase наследует от него и обязан реализовать все абстрактные методы базового класса.

Абстрактные свойства в TypeScript также имеют свои особенности. Они объявляются с ключевым словом abstract и без инициализации, что требует их определения в дочерних классах:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
abstract class Component {
  abstract readonly name: string;
  abstract render(): string;
}
 
class Button extends Component {
  readonly name: string = "button";
  
  render(): string {
    return "<button>Click me</button>";
  }
}
Здесь свойство name должно быть определено в дочернем классе, поскольку оно объявлено как абстрактное. Интересный момент заключается в том, что абстрактные свойства могут иметь модификаторы доступа и дополнительные атрибуты, такие как readonly.

При работе с абстрактными классами в TypeScript существуют определённые ограничения:

1. Невозможность создания экземпляров: Абстрактные классы не могут быть инстанцированы напрямую. Попытка вызова конструктора через new приведёт к ошибке компиляции.
2. Абстрактные методы должны быть реализованы: Дочерние классы обязаны реализовать все абстрактные методы базового класса, иначе они сами должны быть объявлены абстрактными.
3. Статические абстрактные методы: TypeScript не поддерживает объявление статических абстрактных методов, так как по своей природе статические методы принадлежат классу, а не его экземплярам.
4. Приватные абстрактные методы: Нельзя объявить метод одновременно как private и abstract, поскольку дочерние классы должны иметь возможность переопределить абстрактные методы.

TypeScript
1
2
3
4
5
6
7
abstract class Example {
  // Ошибка: абстрактный метод не может быть приватным
  private abstract someMethod(): void;
  
  // Ошибка: статический метод не может быть абстрактным
  static abstract staticMethod(): void;
}
Эти ограничения обоснованы логикой работы абстрактных классов и помогают предотвращать непредсказуемое поведение в коде.

Метаданные и декораторы в TypeScript добавляют дополнительные возможности при работе с абстрактными классами. Декораторы — экспериментальная функция TypeScript, позволяющая аннотировать и модифицировать классы и их члены. Они особенно полезны при создании фреймворков и библиотек, где абстрактные классы часто используются как базовые компоненты.

Жизненный цикл абстрактных классов в TypeScript-приложениях тесно связан с механизмами наследования. На этапе компиляции TypeScript проверяет правильность реализации абстрактных классов и методов. На этапе выполнения (в JavaScript) концепт абстрактности исчезает, так как это чисто TypeScript-конструкция, которая не имеет прямого аналога в JavaScript. После компиляции абстрактные классы превращаются в обычные JavaScript-классы или функции-конструкторы (в зависимости от целевой версии ECMAScript).

Необходимо отметить, что после компиляции TypeScript в JavaScript концепция абстрактности фактически исчезает. Это связано с тем, что JavaScript не имеет встроенной поддержки абстрактных классов на уровне языка. Таким образом, проверки абстрактности происходят только на этапе компиляции TypeScript, а в результирующем JavaScript-коде абстрактные классы становятся обычными классами. Рассмотрим пример преобразования абстрактного класса после компиляции:

TypeScript
1
2
3
4
5
6
7
8
// TypeScript код
abstract class Animal {
  abstract makeSound(): void;
  
  eat(): void {
    console.log('Животное ест');
  }
}
После компиляции в JavaScript (ES6) получится примерно следующее:

JavaScript
1
2
3
4
5
6
// Скомпилированный JavaScript
class Animal {
  eat() {
    console.log('Животное ест');
  }
}
Как видно, информация об абстрактности класса и метода полностью утрачена. Это значит, что защита, которую обеспечивает TypeScript от неправильного использования абстрактных классов, работает только на этапе разработки и компиляции.
Абстрактные классы в TypeScript могут иметь конструкторы, которые вызываются при создании экземпляров дочерних классов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class PaymentProcessor {
  protected transactionId: string;
  
  constructor(transactionId: string) {
    this.transactionId = transactionId;
  }
  
  abstract process(amount: number): boolean;
  
  getTransactionInfo(): string {
    return `Транзакция: ${this.transactionId}`;
  }
}
 
class CreditCardProcessor extends PaymentProcessor {
  constructor(transactionId: string) {
    super(transactionId); // Вызов конструктора базового класса
  }
  
  process(amount: number): boolean {
    console.log(`Обработка платежа по кредитной карте на сумму ${amount}`);
    return true;
  }
}
В этом примере конструктор абстрактного класса PaymentProcessor инициализирует защищённое свойство transactionId. Дочерний класс CreditCardProcessor вызывает этот конструктор через super().
Интересной особенностью TypeScript является возможность объявления абстрактных getter'ов и setter'ов, что расширяет гибкость при проектировании:

TypeScript
1
2
3
4
5
6
7
8
9
abstract class ConfigManager {
  abstract get configPath(): string;
  abstract set configValue(value: any);
  
  loadConfig(): any {
    console.log(`Загрузка конфигурации из ${this.configPath}`);
    return {};
  }
}
В контексте принципов SOLID абстрактные классы играют важную роль в соблюдении принципа подстановки Лисков (Liskov Substitution Principle, LSP), который гласит, что объекты базового класса должны быть заменяемы объектами его дочерних классов без изменения корректности программы.
Полиморфизм — ещё одна фундаментальная концепция ООП, тесно связанная с абстрактными классами. Он позволяет работать с объектами разных классов через единый интерфейс базового класса. Абстрактные классы задают этот интерфейс и часто служат основой для полиморфного поведения:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
abstract class Logger {
  abstract log(message: string): void;
  
  logWithTimestamp(message: string): void {
    const timestamp = new Date().toISOString();
    this.log(`[${timestamp}] ${message}`);
  }
}
 
class ConsoleLogger extends Logger {
  log(message: string): void {
    console.log(message);
  }
}
 
class FileLogger extends Logger {
  constructor(private filePath: string) {
    super();
  }
  
  log(message: string): void {
    console.log(`Запись в файл ${this.filePath}: ${message}`);
    // Реальный код записи в файл...
  }
}
 
// Полиморфное использование
function application(logger: Logger) {
  logger.logWithTimestamp('Приложение запущено');
  // ...
}
 
// Можно использовать любой тип логгера
application(new ConsoleLogger());
application(new FileLogger('app.log'));
В приведённом примере функция application получает параметр типа Logger и может работать с любым конкретным логгером, который наследуется от абстрактного класса. Абстрактные классы также могут включать в себя статические методы и свойства, которые доступны без создания экземпляра класса:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class MathOperations {
  abstract calculate(a: number, b: number): number;
  
  // Статический метод, доступный без создания экземпляра
  static isValidNumber(num: number): boolean {
    return !isNaN(num) && isFinite(num);
  }
}
 
class Addition extends MathOperations {
  calculate(a: number, b: number): number {
    return a + b;
  }
}
 
// Использование статического метода
if (MathOperations.isValidNumber(42)) {
  console.log('Число корректно');
}
Важно отметить взаимодействие абстрактных классов с системой типов TypeScript. Тип, представленный абстрактным классом, может использоваться для аннотации переменных, параметров и возвращаемых значений функций. При этом переменной такого типа может быть присвоен только экземпляр класса, который наследуется от этого абстрактного класса.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class DataSource {
  abstract fetch(): any[];
}
 
class DatabaseSource extends DataSource {
  fetch(): any[] {
    return [{ id: 1, name: 'Запись из базы данных' }];
  }
}
 
class ApiSource extends DataSource {
  fetch(): any[] {
    return [{ id: 2, name: 'Запись из API' }];
  }
}
 
// Типизация через абстрактный класс
let source: DataSource;
source = new DatabaseSource(); // Корректно
source = new ApiSource();      // Корректно
// source = new DataSource();  // Ошибка: нельзя создать экземпляр абстрактного класса
TypeScript поддерживает наследование абстрактных классов от других абстрактных классов, что позволяет создавать сложные иерархии с постепенным уточнением абстракций:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Vehicle {
  abstract move(): void;
}
 
abstract class GroundVehicle extends Vehicle {
  abstract wheels(): number;
}
 
class Car extends GroundVehicle {
  move(): void {
    console.log('Машина едет по дороге');
  }
  
  wheels(): number {
    return 4;
  }
}
В данном примере класс Car должен реализовать как move() из Vehicle, так и wheels() из GroundVehicle.

Абстрактные классы в TypeScript становятся особенно полезными при создании базовых компонентов для фреймворков и библиотек. Они обеспечивают гибкую систему наследования, которая позволяет разработчикам создавать специализированные версии компонентов, сохраняя при этом согласованный интерфейс и поведение. При проектировании абстрактных классов рекомендуется следовать принципу минимальной достаточности: включать в абстрактный класс только те методы и свойства, которые действительно общие для всех дочерних классов. Избыточный функционал в абстрактном классе может привести к нарушению принципа интерфейсной сегрегации (Interface Segregation Principle) из SOLID.

Практическое применение



Понимание теоретических основ абстрактных классов в TypeScript – лишь первый шаг к их эффективному использованию. В реальных проектах абстрактные классы становятся мощным инструментом для создания гибких и расширяемых архитектур. Рассмотрим конкретные примеры их практического применения.

Создание базовых шаблонов с абстрактными методами



Классический сценарий использования абстрактных классов – создание базовых шаблонов для группы похожих компонентов. В качестве примера реализуем систему обработки платежей:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
abstract class PaymentProcessor {
  constructor(protected orderId: string) {}
 
  // Общий алгоритм обработки платежа
  processPayment(amount: number): boolean {
    if (!this.validateAmount(amount)) {
      return false;
    }
    
    this.preparePayment();
    const success = this.executePayment(amount);
    
    if (success) {
      this.finalizePayment();
    }
    
    return success;
  }
 
  // Методы с реализацией
  protected validateAmount(amount: number): boolean {
    return amount > 0;
  }
  
  protected preparePayment(): void {
    console.log(`Подготовка платежа для заказа ${this.orderId}`);
  }
  
  protected finalizePayment(): void {
    console.log(`Финализация платежа для заказа ${this.orderId}`);
  }
 
  // Абстрактный метод для реализации в подклассах
  protected abstract executePayment(amount: number): boolean;
}
Теперь создадим конкретные реализации процессоров для разных платёжных систем:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CreditCardProcessor extends PaymentProcessor {
  constructor(
    orderId: string,
    private readonly cardNumber: string,
    private readonly cvv: string
  ) {
    super(orderId);
  }
 
  protected executePayment(amount: number): boolean {
    console.log(`Обработка платежа на сумму ${amount} с карты ${this.cardNumber}`);
    // Логика обработки платежа через кредитную карту
    return true;
  }
}
 
class PayPalProcessor extends PaymentProcessor {
  constructor(
    orderId: string,
    private readonly email: string
  ) {
    super(orderId);
  }
 
  protected executePayment(amount: number): boolean {
    console.log(`Обработка платежа на сумму ${amount} через PayPal (${this.email})`);
    // Логика обработки платежа через PayPal
    return true;
  }
}
В этом примере абстрактный класс PaymentProcessor определяет общую структуру и поведение для всех обработчиков платежей, а конкретные реализации заполняют недостающие части. Такой подход обеспечивает единообразие в обработке всех типов платежей и позволяет легко добавлять новые методы оплаты.

Примеры наследования и реализации



Другая распространённая область применения абстрактных классов – создание компонентов пользовательского интерфейса:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
abstract class UIComponent {
  protected element: HTMLElement | null = null;
 
  constructor(protected readonly id: string) {}
 
  render(): void {
    if (!this.element) {
      this.element = this.createElement();
      this.setupEventListeners();
    }
    document.getElementById('app')?.appendChild(this.element);
  }
 
  hide(): void {
    this.element?.remove();
  }
 
  // Абстрактный метод создания DOM-элемента
  protected abstract createElement(): HTMLElement;
 
  // Метод с дефолтной реализацией
  protected setupEventListeners(): void {
    // Пустая реализация по умолчанию
  }
}
 
class Button extends UIComponent {
  constructor(id: string, private readonly text: string) {
    super(id);
  }
 
  protected createElement(): HTMLElement {
    const button = document.createElement('button');
    button.id = this.id;
    button.textContent = this.text;
    return button;
  }
 
  protected setupEventListeners(): void {
    this.element?.addEventListener('click', () => {
      console.log(`Кнопка ${this.id} нажата`);
    });
  }
}
 
class Image extends UIComponent {
  constructor(id: string, private readonly src: string, private readonly alt: string) {
    super(id);
  }
 
  protected createElement(): HTMLElement {
    const img = document.createElement('img');
    img.id = this.id;
    img.src = this.src;
    img.alt = this.alt;
    return img;
  }
}
Здесь абстрактный класс UIComponent предоставляет общую функциональность для всех UI-компонентов, при этом требуя от дочерних классов определить метод создания DOM-элемента. Класс Button переопределяет также метод настройки обработчиков событий, а класс Image использует его дефолтную реализацию.

Типичные сценарии использования



Абстрактные классы в TypeScript особенно полезны в следующих случаях:

1. Создание каркаса для семейства классов – когда у вас есть группа классов с общей функциональностью, но различной реализацией некоторых операций.

2. Построение слоёв абстракции – для отделения интерфейса от реализации и обеспечения гибкости.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Repository<T> {
  abstract getById(id: string): Promise<T | null>;
  abstract getAll(): Promise<T[]>;
  abstract create(item: T): Promise<T>;
  abstract update(id: string, item: T): Promise<T | null>;
  abstract delete(id: string): Promise<boolean>;
 
  // Общая функциональность
  async exists(id: string): Promise<boolean> {
    const item = await this.getById(id);
    return item !== null;
  }
}
 
// Конкретные реализации могут работать с разными хранилищами данных
class MongoUserRepository extends Repository<User> {
  // Реализация методов для MongoDB
}
 
class PostgresUserRepository extends Repository<User> {
  // Реализация методов для PostgreSQL
}
3. Создание расширяемых фреймворков – для обеспечения точек расширения в библиотеках и фреймворках.

4. Реализация общих алгоритмов с вариативными шагами – когда основная последовательность действий фиксирована, но отдельные шаги могут различаться.

Реализация шаблона "Шаблонный метод" с помощью абстрактных классов



Паттерн "Шаблонный метод" идеально подходит для реализации через абстрактные классы. Этот паттерн определяет скелет алгоритма в базовом классе, позволяя подклассам переопределять конкретные шаги без изменения общей структуры.
Рассмотрим пример обработки документов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
abstract class DocumentProcessor {
  // Шаблонный метод
  process(document: string): string {
    const validDoc = this.validate(document);
    if (!validDoc) {
      throw new Error('Invalid document');
    }
 
    const preprocessed = this.preprocess(validDoc);
    const processed = this.processContent(preprocessed);
    return this.postprocess(processed);
  }
 
  // Методы с дефолтной реализацией
  protected validate(document: string): string | null {
    return document.trim() ? document : null;
  }
 
  protected preprocess(document: string): string {
    return document.trim();
  }
 
  // Абстрактный метод - ключевой шаг алгоритма
  protected abstract processContent(document: string): string;
 
  protected postprocess(document: string): string {
    return document;
  }
}
 
class HTMLDocumentProcessor extends DocumentProcessor {
  protected processContent(document: string): string {
    return `<div>${document}</div>`;
  }
 
  protected postprocess(document: string): string {
    return `<!DOCTYPE html><html><body>${document}</body></html>`;
  }
}
 
class MarkdownDocumentProcessor extends DocumentProcessor {
  protected processContent(document: string): string {
    return document
      .replace(/# (.+)/g, '<h1>$1</h1>')
      .replace(/\*\*(.+)\*\*/g, '<strong>$1</strong>');
  }
}
В этом примере метод process() определяет общий алгоритм обработки документов. Каждый шаг алгоритма представлен отдельным методом, где processContent является абстрактным и требует реализации в дочерних классах. Использовать эти классы можно следующим образом:

TypeScript
1
2
3
4
5
const htmlProcessor = new HTMLDocumentProcessor();
const mdProcessor = new MarkdownDocumentProcessor();
 
const htmlResult = htmlProcessor.process('Текст документа');
const mdResult = mdProcessor.process('# Заголовок\n[B]Жирный текст[/B]');
Данный подход следует принципу "Голливуда" (Hollywood Principle): "Не звоните нам, мы позвоним вам". Базовый класс контролирует общий поток выполнения и вызывает методы дочерних классов в нужные моменты, а не наоборот.

Стратегии тестирования кода с абстрактными классами



Тестирование кода, использующего абстрактные классы, представляет определённые сложности из-за невозможности создать экземпляры этих классов напрямую. Однако существует несколько эффективных подходов к решению этой проблемы.
Один из наиболее распространённых методов – создание тестовых реализаций абстрактных классов специально для юнит-тестов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
abstract class DataLoader {
  abstract load(id: string): Promise<any>;
  
  async loadWithRetry(id: string, retries = 3): Promise<any> {
    let lastError;
    for (let i = 0; i < retries; i++) {
      try {
        return await this.load(id);
      } catch (error) {
        lastError = error;
        await this.delay(100 * Math.pow(2, i));
      }
    }
    throw lastError;
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
 
// Тестовая реализация для юнит-тестирования
class TestDataLoader extends DataLoader {
  private failCount = 0;
  constructor(private data: any, private shouldFailTimes = 0) {
    super();
  }
  
  async load(id: string): Promise<any> {
    if (this.failCount < this.shouldFailTimes) {
      this.failCount++;
      throw new Error('Симуляция сбоя загрузки');
    }
    return this.data;
  }
}
 
// Использование в тестах
describe('DataLoader', () => {
  it('должен успешно загрузить данные при первой попытке', async () => {
    const loader = new TestDataLoader({ id: '123', name: 'Test' });
    const result = await loader.loadWithRetry('123');
    expect(result).toEqual({ id: '123', name: 'Test' });
  });
  
  it('должен повторить попытку при сбое', async () => {
    const loader = new TestDataLoader({ id: '123', name: 'Test' }, 2);
    const result = await loader.loadWithRetry('123');
    expect(result).toEqual({ id: '123', name: 'Test' });
  });
});
Другой подход – использование техники имитации (моков) и частичной имитации для тестирования абстрактных методов и их взаимодействия с конкретными методами:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
abstract class EventHandler {
  abstract handleEvent(event: any): void;
  
  processEvent(event: any): void {
    console.log(`Обработка события: ${event.type}`);
    this.preProcess(event);
    this.handleEvent(event);
    this.postProcess(event);
  }
  
  protected preProcess(event: any): void {
    event.timestamp = new Date();
  }
  
  protected postProcess(event: any): void {
    console.log(`Событие обработано в ${event.timestamp}`);
  }
}
 
// Используя Jest для тестирования
test('processEvent должен вызывать методы в правильном порядке', () => {
  // Создаём частичный мок абстрактного класса
  const handler = {
    handleEvent: jest.fn(),
    preProcess: jest.fn(),
    postProcess: jest.fn(),
    processEvent: EventHandler.prototype.processEvent
  };
  
  const event = { type: 'click' };
  handler.processEvent(event);
  
  // Проверяем порядок вызовов
  expect(handler.preProcess).toHaveBeenCalledBefore(handler.handleEvent);
  expect(handler.handleEvent).toHaveBeenCalledBefore(handler.postProcess);
});
При тестировании кода, использующего абстрактные классы, также полезно сосредоточиться на тестировании конкретных реализаций и их взаимодействия с функциональностью базового класса, а не на самом абстрактном классе.

Реализация полиморфизма через абстрактные классы



Абстрактные классы – мощный инструмент для реализации полиморфизма в TypeScript. Они позволяют создавать обобщённые интерфейсы для работы с различными реализациями одной концепции. Рассмотрим пример системы рендеринга графических примитивов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
abstract class Shape {
  constructor(protected color: string) {}
  
  abstract draw(context: CanvasRenderingContext2D): void;
  abstract calculateArea(): number;
  
  getColor(): string {
    return this.color;
  }
}
 
class Circle extends Shape {
  constructor(color: string, private radius: number) {
    super(color);
  }
  
  draw(context: CanvasRenderingContext2D): void {
    context.fillStyle = this.color;
    context.beginPath();
    context.arc(0, 0, this.radius, 0, Math.PI * 2);
    context.fill();
  }
  
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}
 
class Rectangle extends Shape {
  constructor(color: string, private width: number, private height: number) {
    super(color);
  }
  
  draw(context: CanvasRenderingContext2D): void {
    context.fillStyle = this.color;
    context.fillRect(-this.width/2, -this.height/2, this.width, this.height);
  }
  
  calculateArea(): number {
    return this.width * this.height;
  }
}
 
// Полиморфное использование
function drawShapes(context: CanvasRenderingContext2D, shapes: Shape[]): void {
  shapes.forEach(shape => {
    context.save();
    shape.draw(context);
    context.restore();
    
    console.log(`Фигура площадью ${shape.calculateArea()} нарисована`);
  });
}
 
// Создание и использование разных фигур
const shapes: Shape[] = [
  new Circle('red', 50),
  new Rectangle('blue', 100, 80)
];
 
// Получение контекста canvas и отрисовка фигур
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d')!;
drawShapes(context, shapes);
В этом примере функция drawShapes работает с любым объектом, реализующим абстрактный класс Shape, не зная его конкретного типа. Это классический пример полиморфизма – способности переменной ссылаться на объекты разных типов и вызывать их методы, которые ведут себя по-разному в зависимости от конкретного типа.
Полиморфизм, реализованный через абстрактные классы, имеет следующие преимущества:

1. Безопасность типов – TypeScript гарантирует, что все дочерние классы реализуют необходимые абстрактные методы.
2. Возможность обеспечить как общую функциональность, так и специфичное поведение.
3. Удобство при рефакторинге – добавление нового метода в абстрактный класс автоматически делает его доступным для всех дочерних классов.

Обработка ошибок при работе с абстрактными классами



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

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
abstract class APIClient {
  abstract fetch<T>(endpoint: string): Promise<T>;
  
  async fetchWithErrorHandling<T>(endpoint: string): Promise<T> {
    try {
      return await this.fetch<T>(endpoint);
    } catch (error) {
      this.handleError(error);
      throw error;
    }
  }
  
  protected handleError(error: any): void {
    console.error(`API request failed: ${error.message}`);
  }
}
 
class RESTClient extends APIClient {
  async fetch<T>(endpoint: string): Promise<T> {
    // Реализация запроса
    const response = await fetch(`https://api.example.com/${endpoint}`);
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  }
  
  // Переопределение метода обработки ошибок
  protected handleError(error: any): void {
    super.handleError(error);
    // Дополнительная логика обработки ошибок
    if (error.message.includes('401')) {
      console.error('Unauthorized access. Please login again.');
    }
  }
}
В этом примере абстрактный класс APIClient предоставляет общий механизм обработки ошибок для всех клиентов API. Конкретный класс RESTClient расширяет базовую обработку ошибок с дополнительной логикой для определённых случаев. При проектировании иерархии классов с абстрактными классами следует учитывать следующие рекомендации по обработке ошибок:

1. Предоставьте базовые механизмы обработки ошибок в абстрактном классе, которые можно переопределить в дочерних классах.
2. Используйте явное приведение типов с проверкой, если вам требуется работать с конкретными типами:

TypeScript
1
2
3
4
5
6
7
8
9
10
function processShape(shape: Shape): void {
  // Безопасная проверка типа перед приведением
  if (shape instanceof Circle) {
    // Теперь TypeScript знает, что shape - это Circle
    console.log(`Работаем с кругом радиуса ${shape.getRadius()}`);
  } else if (shape instanceof Rectangle) {
    // shape - это Rectangle
    console.log(`Работаем с прямоугольником ${shape.getWidth()}x${shape.getHeight()}`);
  }
}
3. Создавайте специализированные исключения для различных ситуаций:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}
 
abstract class Validator<T> {
  abstract validate(data: T): boolean;
  
  validateOrThrow(data: T): void {
    if (!this.validate(data)) {
      throw new ValidationError('Данные не прошли валидацию');
    }
  }
}
Такой подход к обработке ошибок делает код более надёжным и помогает быстрее выявлять проблемы при разработке и отладке приложений, использующих абстрактные классы.
В продолжение темы обработки ошибок, важно отметить особенности работы с абстрактными классами в асинхронном контексте. При асинхронных операциях часто возникают сложности, связанные с обработкой исключений в дочерних классах:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
abstract class AsyncDataProvider<T> {
  abstract fetchData(): Promise<T>;
  
  async getDataSafely(): Promise<T | null> {
    try {
      return await this.fetchData();
    } catch (error) {
      this.logError(error);
      return null;
    }
  }
  
  protected logError(error: unknown): void {
    console.error(`Ошибка при получении данных: ${error instanceof Error ? error.message : String(error)}`);
  }
}
 
class RemoteDataProvider<T> extends AsyncDataProvider<T> {
  constructor(private url: string) {
    super();
  }
  
  async fetchData(): Promise<T> {
    const response = await fetch(this.url);
    if (!response.ok) {
      throw new Error(`HTTP ошибка: ${response.status}`);
    }
    return await response.json();
  }
  
  protected override logError(error: unknown): void {
    super.logError(error);
    
    // Дополнительная обработка для сетевых ошибок
    if (error instanceof TypeError && error.message.includes('network')) {
      console.error('Проверьте подключение к сети!');
    }
  }
}
Здесь базовый класс AsyncDataProvider предоставляет общий механизм обработки асинхронных ошибок, а дочерний класс специализирует его для сетевых запросов.

Практические паттерны с использованием абстрактных классов



Абстрактные классы особенно эффективны при реализации паттерна "Фабричный метод" (Factory Method), где они определяют интерфейс для создания объектов, но позволяют подклассам решать, какой класс инстанцировать:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
abstract class DocumentCreator {
  // Фабричный метод
  abstract createDocument(name: string, content: string): Document;
  
  // Общая логика работы с документами
  saveDocument(name: string, content: string, location: string): void {
    const document = this.createDocument(name, content);
    console.log(`Сохранение документа "${document.getName()}" по пути: ${location}`);
    document.save(location);
  }
}
 
interface Document {
  getName(): string;
  getContent(): string;
  save(location: string): void;
}
 
class PDFCreator extends DocumentCreator {
  createDocument(name: string, content: string): Document {
    console.log(`Создание PDF-документа: ${name}`);
    return new PDFDocument(name, content);
  }
}
 
class WordCreator extends DocumentCreator {
  createDocument(name: string, content: string): Document {
    console.log(`Создание Word-документа: ${name}`);
    return new WordDocument(name, content);
  }
}
 
class PDFDocument implements Document {
  constructor(private name: string, private content: string) {}
  
  getName(): string {
    return `${this.name}.pdf`;
  }
  
  getContent(): string {
    return this.content;
  }
  
  save(location: string): void {
    console.log(`PDF-документ сохранён в ${location}/${this.getName()}`);
  }
}
 
class WordDocument implements Document {
  constructor(private name: string, private content: string) {}
  
  getName(): string {
    return `${this.name}.docx`;
  }
  
  getContent(): string {
    return this.content;
  }
  
  save(location: string): void {
    console.log(`Word-документ сохранён в ${location}/${this.getName()}`);
  }
}
Этот паттерн позволяет создавать документы разных типов, используя общий интерфейс, при этом конкретный тип создаваемого документа определяется подклассом.

Применение стратегии зависимостей в абстрактных классах



Абстрактные классы могут использоваться для реализации стратегии внедрения зависимостей, что улучшает тестируемость и гибкость кода:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
abstract class ChatClient {
  abstract sendMessage(message: string, receiver: string): Promise<boolean>;
  abstract receiveMessages(): Promise<string[]>;
  
  // Общая функциональность для всех чат-клиентов
  async sendBroadcast(message: string, receivers: string[]): Promise<string[]> {
    const failedReceivers: string[] = [];
    
    for (const receiver of receivers) {
      try {
        const success = await this.sendMessage(message, receiver);
        if (!success) {
          failedReceivers.push(receiver);
        }
      } catch (error) {
        console.error(`Ошибка при отправке сообщения для ${receiver}:`, error);
        failedReceivers.push(receiver);
      }
    }
    
    return failedReceivers;
  }
}
 
// Реальная реализация для продакшн
class WebSocketChatClient extends ChatClient {
  constructor(private socket: WebSocket) {
    super();
  }
  
  async sendMessage(message: string, receiver: string): Promise<boolean> {
    // Реальная логика отправки через WebSocket
    this.socket.send(JSON.stringify({ to: receiver, text: message }));
    return true;
  }
  
  async receiveMessages(): Promise<string[]> {
    // Логика получения сообщений
    return [];
  }
}
 
// Мок для тестирования
class MockChatClient extends ChatClient {
  private sentMessages: Array<{message: string, receiver: string}> = [];
  private messagesToReceive: string[] = [];
  
  setSentMessages(messages: Array<{message: string, receiver: string}>): void {
    this.sentMessages = messages;
  }
  
  setMessagesToReceive(messages: string[]): void {
    this.messagesToReceive = messages;
  }
  
  async sendMessage(message: string, receiver: string): Promise<boolean> {
    this.sentMessages.push({ message, receiver });
    return true;
  }
  
  async receiveMessages(): Promise<string[]> {
    return this.messagesToReceive;
  }
  
  getSentMessagesCount(): number {
    return this.sentMessages.length;
  }
}
Такой подход позволяет легко заменять реальную реализацию на тестовую или альтернативную в зависимости от окружения.

Комбинирование абстрактных классов с дженериками



Объединение абстрактных классов с дженериками (обобщёнными типами) в TypeScript создаёт особенно мощные абстракции:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
abstract class CrudService<T, ID> {
  abstract findAll(): Promise<T[]>;
  abstract findById(id: ID): Promise<T | null>;
  abstract create(entity: Omit<T, 'id'>): Promise<T>;
  abstract update(id: ID, entity: Partial<T>): Promise<T>;
  abstract delete(id: ID): Promise<boolean>;
  
  // Общая функциональность для любого типа
  async exists(id: ID): Promise<boolean> {
    const entity = await this.findById(id);
    return entity !== null;
  }
  
  async findOrCreate(id: ID, defaultEntity: Omit<T, 'id'>): Promise<T> {
    const existing = await this.findById(id);
    if (existing) {
      return existing;
    }
    
    return await this.create(defaultEntity);
  }
}
 
interface User {
  id: number;
  username: string;
  email: string;
}
 
class UserService extends CrudService<User, number> {
  private users: User[] = [];
  
  async findAll(): Promise<User[]> {
    return [...this.users];
  }
  
  async findById(id: number): Promise<User | null> {
    return this.users.find(user => user.id === id) || null;
  }
  
  async create(entity: Omit<User, 'id'>): Promise<User> {
    const newId = this.users.length > 0 
      ? Math.max(...this.users.map(u => u.id)) + 1 
      : 1;
    
    const newUser = { ...entity, id: newId } as User;
    this.users.push(newUser);
    return newUser;
  }
  
  async update(id: number, entity: Partial<User>): Promise<User> {
    const index = this.users.findIndex(user => user.id === id);
    if (index === -1) {
      throw new Error(`User with id ${id} not found`);
    }
    
    this.users[index] = { ...this.users[index], ...entity };
    return this.users[index];
  }
  
  async delete(id: number): Promise<boolean> {
    const initialLength = this.users.length;
    this.users = this.users.filter(user => user.id !== id);
    return initialLength !== this.users.length;
  }
}
В этом примере абстрактный класс CrudService параметризован типами T (тип сущности) и ID (тип идентификатора), что позволяет создавать сервисы для работы с разными типами данных, сохраняя при этом общую структуру и функциональность.

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

Продвинутые техники



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

Комбинирование с другими возможностями TypeScript



Абстрактные классы становятся особенно полезными, когда их сочетают с другими продвинутыми функциями TypeScript. Одна из таких комбинаций — абстрактные классы с условными типами и маппированными типами:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type EntityFields<T> = {
  [K in keyof T]: T[K] extends Function ? never : K
}[keyof T];
 
abstract class Entity<T> {
  abstract get id(): string | number;
  
  getFieldValue<K extends EntityFields<T>>(fieldName: K): T[K] {
    return (this as unknown as T)[fieldName];
  }
  
  abstract clone(): T;
  
  serialize(): Record<string, any> {
    const result: Record<string, any> = {};
    
    // Получаем все ключи объекта
    const keys = Object.keys(this) as Array<keyof this>;
    
    // Фильтруем только данные, исключая методы
    for (const key of keys) {
      if (typeof this[key] !== 'function') {
        result[key as string] = this[key];
      }
    }
    
    return result;
  }
}
 
class User extends Entity<User> {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string
  ) {
    super();
  }
  
  clone(): User {
    return new User(this.id, this.name, this.email);
  }
}
В этом примере используются продвинутые возможности системы типов TypeScript — условные типы и маппированные типы для извлечения только полей данных (не методов) из дженерик-типа. Метод getFieldValue позволяет типобезопасно получать значения любого поля данных.
Другой интересный пример — комбинирование абстрактных классов с декораторами:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function log<T extends abstract new (...args: any[]) => any>(target: T) {
  // Создаём новый класс, расширяющий целевой абстрактный класс
  abstract class LoggedClass extends target {
    constructor(...args: any[]) {
      console.log(`Создание экземпляра класса ${target.name}`);
      super(...args);
      console.log(`Экземпляр класса ${target.name} создан`);
    }
  }
  
  return LoggedClass;
}
 
@log
abstract class Service {
  abstract execute(): void;
  
  initialize(): void {
    console.log('Инициализация сервиса');
  }
}
 
class PaymentService extends Service {
  execute(): void {
    console.log('Выполнение платежного сервиса');
  }
}
 
const service = new PaymentService();
service.initialize(); // Выведет логи создания и инициализации
service.execute();    // Выполнение сервиса
Декоратор log добавляет логирование к конструктору любого класса, включая абстрактные. Это пример модификации поведения абстрактного класса без изменения его исходного кода.

Паттерны проектирования на основе абстрактных классов



Абстрактные классы лежат в основе многих паттернов проектирования. Кроме уже рассмотренных "Шаблонного метода" и "Фабричного метода", они часто используются в паттерне "Строитель" (Builder):

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
abstract class QueryBuilder<T> {
  protected conditions: string[] = [];
  protected sortings: string[] = [];
  protected limitValue: number | null = null;
  protected offsetValue: number | null = null;
  
  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }
  
  orderBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.sortings.push(`${field} ${direction}`);
    return this;
  }
  
  limit(count: number): this {
    this.limitValue = count;
    return this;
  }
  
  offset(count: number): this {
    this.offsetValue = count;
    return this;
  }
  
  // Абстрактный метод, который должен быть реализован в подклассах
  abstract build(): string;
  
  // Абстрактный метод выполнения запроса
  abstract execute(): Promise<T[]>;
}
 
class SQLQueryBuilder<T> extends QueryBuilder<T> {
  constructor(private table: string, private dbConnection: any) {
    super();
  }
  
  build(): string {
    let query = `SELECT * FROM ${this.table}`;
    
    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    
    if (this.sortings.length > 0) {
      query += ` ORDER BY ${this.sortings.join(', ')}`;
    }
    
    if (this.limitValue !== null) {
      query += ` LIMIT ${this.limitValue}`;
    }
    
    if (this.offsetValue !== null) {
      query += ` OFFSET ${this.offsetValue}`;
    }
    
    return query;
  }
  
  async execute(): Promise<T[]> {
    const query = this.build();
    return this.dbConnection.query(query);
  }
}
В этом примере абстрактный класс QueryBuilder определяет интерфейс и базовую функциональность для построения запросов, а конкретный класс SQLQueryBuilder реализует построение SQL-запросов.

Другой распространённый паттерн — "Компоновщик" (Composite) — также может быть реализован с помощью абстрактных классов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
abstract class FileSystemItem {
  constructor(protected name: string) {}
  
  abstract getSize(): number;
  abstract print(indent: string): void;
  
  getName(): string {
    return this.name;
  }
}
 
class File extends FileSystemItem {
  constructor(name: string, private size: number) {
    super(name);
  }
  
  getSize(): number {
    return this.size;
  }
  
  print(indent: string): void {
    console.log(`${indent} ${this.name} (${this.size} bytes)`);
  }
}
 
class Directory extends FileSystemItem {
  private items: FileSystemItem[] = [];
  
  addItem(item: FileSystemItem): void {
    this.items.push(item);
  }
  
  getSize(): number {
    return this.items.reduce((sum, item) => sum + item.getSize(), 0);
  }
  
  print(indent: string): void {
    console.log(`${indent} ${this.name} (${this.getSize()} bytes)`);
    this.items.forEach(item => item.print(indent + '  '));
  }
}
Паттерн Компоновщик позволяет обращаться с отдельными объектами и группами объектов одинаково. В этом примере абстрактный класс FileSystemItem определяет общий интерфейс для файлов и директорий.

Оптимизация кода с использованием абстрактных классов



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

1. Уменьшение дублирования кода. Абстрактные классы позволяют вынести общую функциональность в базовый класс, избегая копирования кода в нескольких классах:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
abstract class NetworkRequest {
  protected baseUrl: string;
  
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }
  
  protected async fetchWithRetry<T>(
    url: string, 
    options: RequestInit = {}, 
    retries = 3
  ): Promise<T> {
    let lastError;
    
    for (let i = 0; i < retries; i++) {
      try {
        const response = await fetch(url, options);
        
        if (!response.ok) {
          throw new Error(`HTTP error: ${response.status}`);
        }
        
        return await response.json();
      } catch (error) {
        lastError = error;
        // Экспоненциальная задержка между попытками
        await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, i)));
      }
    }
    
    throw lastError;
  }
  
  abstract get<T>(endpoint: string, params?: Record<string, string>): Promise<T>;
  abstract post<T, D>(endpoint: string, data: D): Promise<T>;
}
2. Организация вертикальных слоёв абстракции. Абстрактные классы помогают создавать четкую иерархию с последовательным повышением уровня абстракции:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
abstract class DataSource<T> {
  abstract fetch(): Promise<T[]>;
}
 
abstract class CachingDataSource<T> extends DataSource<T> {
  private cache: T[] | null = null;
  private lastFetchTime: number = 0;
  private readonly cacheTTL: number;
  
  constructor(cacheTTL: number = 60000) { // TTL в миллисекундах
    super();
    this.cacheTTL = cacheTTL;
  }
  
  async fetch(): Promise<T[]> {
    const now = Date.now();
    
    if (this.cache && now - this.lastFetchTime < this.cacheTTL) {
      return this.cache;
    }
    
    const data = await this.fetchFresh();
    this.cache = data;
    this.lastFetchTime = now;
    
    return data;
  }
  
  abstract fetchFresh(): Promise<T[]>;
  
  clearCache(): void {
    this.cache = null;
  }
}
 
class UserApiDataSource extends CachingDataSource<User> {
  constructor(private apiClient: ApiClient, cacheTTL?: number) {
    super(cacheTTL);
  }
  
  async fetchFresh(): Promise<User[]> {
    return this.apiClient.get<User[]>('/users');
  }
}
В этой иерархии класс DataSource определяет самый базовый интерфейс, CachingDataSource добавляет функциональность кеширования, а UserApiDataSource уже содержит конкретную реализацию для пользователей.

Абстрактные классы в контексте архитектуры приложения



При проектировании архитектуры приложения абстрактные классы часто используются для определения слоя абстракции между различными частями системы:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Уровень доступа к данным
abstract class Repository<T extends { id: string | number }> {
  abstract findById(id: T['id']): Promise<T | null>;
  abstract findAll(): Promise<T[]>;
  abstract save(entity: T): Promise<T>;
  abstract delete(id: T['id']): Promise<boolean>;
}
 
// Уровень бизнес-логики
abstract class Service<T extends { id: string | number }> {
  constructor(protected repository: Repository<T>) {}
  
  async findById(id: T['id']): Promise<T | null> {
    return this.repository.findById(id);
  }
  
  async findAll(): Promise<T[]> {
    return this.repository.findAll();
  }
  
  abstract validate(entity: T): boolean;
  
  async save(entity: T): Promise<T> {
    if (!this.validate(entity)) {
      throw new Error('Validation failed');
    }
    
    return this.repository.save(entity);
  }
}
 
// Уровень представления
abstract class Controller<T extends { id: string | number }> {
  constructor(protected service: Service<T>) {}
  
  abstract transformToDTO(entity: T): any;
  
  async getById(id: T['id']): Promise<any | null> {
    const entity = await this.service.findById(id);
    return entity ? this.transformToDTO(entity) : null;
  }
  
  async getAll(): Promise<any[]> {
    const entities = await this.service.findAll();
    return entities.map(entity => this.transformToDTO(entity));
  }
}
Такая структура обеспечивает четкое разделение ответственности между слоями и удобный механизм для замены конкретных реализаций.

Миграция с интерфейсов на абстрактные классы: плюсы и минусы



В проектах на TypeScript часто возникает вопрос о выборе между интерфейсами и абстрактными классами. Иногда бывает необходимо мигрировать с одного подхода на другой. Рассмотрим процесс миграции с интерфейсов на абстрактные классы, выделяя преимущества и недостатки такого перехода. Типичный интерфейс в TypeScript выглядит следующим образом:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Logger {
  log(message: string): void;
  error(message: string, error?: Error): void;
  warn(message: string): void;
  debug?(data: any): void;
}
 
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
  
  error(message: string, error?: Error): void {
    console.error(`[ERROR]: ${message}`, error);
  }
  
  warn(message: string): void {
    console.warn(`[WARN]: ${message}`);
  }
}
При миграции на абстрактный класс код может трансформироваться следующим образом:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
abstract class Logger {
  abstract log(message: string): void;
  abstract error(message: string, error?: Error): void;
  abstract warn(message: string): void;
  
  // Добавляем общую реализацию
  debug(data: any): void {
    console.log(`[DEBUG]:`, data);
  }
  
  // Добавляем общую функциональность
  logWithTimestamp(message: string): void {
    const timestamp = new Date().toISOString();
    this.log(`[${timestamp}] ${message}`);
  }
}
 
class ConsoleLogger extends Logger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
  
  error(message: string, error?: Error): void {
    console.error(`[ERROR]: ${message}`, error);
  }
  
  warn(message: string): void {
    console.warn(`[WARN]: ${message}`);
  }
}
Преимущества миграции на абстрактные классы:

1. Возможность предоставить общую функциональность. В примере выше мы добавили метод logWithTimestamp, который автоматически становится доступен во всех дочерних классах.
2. Поддержка конструкторов и состояния. Абстрактные классы могут иметь конструкторы и поля для хранения состояния:
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class ConfigurableLogger extends Logger {
  constructor(protected logLevel: 'debug' | 'info' | 'warn' | 'error') {
    super();
  }
  
  shouldLog(level: string): boolean {
    const levels = ['debug', 'info', 'warn', 'error'];
    const currentLevelIndex = levels.indexOf(this.logLevel);
    const targetLevelIndex = levels.indexOf(level);
    
    return targetLevelIndex >= currentLevelIndex;
  }
}
3. Возможность определить защищённые методы и свойства. В абстрактных классах можно использовать модификаторы доступа protected и private, что невозможно в интерфейсах.
4. Упрощение рефакторинга. Добавление нового метода в абстрактный класс не требует изменений в дочерних классах, если метод имеет реализацию по умолчанию.

Недостатки миграции на абстрактные классы:

1. Потеря множественного наследования. TypeScript не поддерживает множественное наследование классов, в то время как класс может реализовывать множество интерфейсов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface Loggable {
  log(message: string): void;
}
 
interface Serializable {
  serialize(): string;
}
 
// Можно реализовать оба интерфейса
class JsonLogger implements Loggable, Serializable {
  log(message: string): void {
    console.log(message);
  }
  
  serialize(): string {
    return JSON.stringify(this);
  }
}
 
// А вот так уже не получится
abstract class Logger {
  abstract log(message: string): void;
}
 
abstract class Serializer {
  abstract serialize(): string;
}
 
// Ошибка: класс может наследовать только от одного класса
// class JsonLogger extends Logger, Serializer { ... }
2. Увеличение связанности кода. Абстрактные классы создают более тесную связь между базовым и дочерними классами, что может затруднить изменения в будущем.

3. Сложности при тестировании. Код, использующий абстрактные классы, может оказаться сложнее в тестировании из-за более тесной связи между классами.

Альтернативные подходы к реализации абстрактных концепций в TypeScript



Помимо стандартных абстрактных классов, TypeScript предлагает несколько альтернативных подходов к реализации абстрактных концепций.

1. Комбинация интерфейсов и классов с реализацией по умолчанию:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface Repository<T> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
  save(entity: T): Promise<T>;
}
 
// Класс с реализацией по умолчанию
class BaseRepository<T> implements Repository<T> {
  async findAll(): Promise<T[]> {
    throw new Error('Not implemented');
  }
  
  async findById(id: string): Promise<T | null> {
    throw new Error('Not implemented');
  }
  
  async save(entity: T): Promise<T> {
    throw new Error('Not implemented');
  }
}
 
// Использование через композицию
class UserRepository implements Repository<User> {
  private base = new BaseRepository<User>();
  
  async findAll(): Promise<User[]> {
    // Собственная реализация
    return [];
  }
  
  async findById(id: string): Promise<User | null> {
    // Использование базовой реализации
    return this.base.findById(id);
  }
  
  async save(entity: User): Promise<User> {
    // Собственная реализация
    return entity;
  }
}
2. Миксины для смешивания функциональности:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Constructor<T = {}> = new (...args: any[]) => T;
 
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
    
    update() {
      this.updatedAt = new Date();
    }
  };
}
 
function Loggable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    log(message: string) {
      console.log(`[${new Date().toISOString()}] ${message}`);
    }
  };
}
 
class Entity {}
 
// Создаём класс с примешанным функционалом
const TimestampedLoggableEntity = Loggable(Timestamped(Entity));
 
// Используем полученный класс
const entity = new TimestampedLoggableEntity();
entity.log('Создана новая сущность');
console.log(entity.createdAt);
Миксины позволяют комбинировать функциональность из разных источников, что частично решает проблему отсутствия множественного наследования.

3. Использование инверсии зависимостей и инъекции:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Logger {
  log(message: string): void {
    console.log(message);
  }
}
 
class Service {
  constructor(private logger: Logger) {}
  
  execute(): void {
    this.logger.log('Выполнение сервиса');
  }
}
 
// Можно заменить логгер на любую совместимую реализацию
const service = new Service(new Logger());
service.execute();
Этот подход фокусируется на композиции объектов вместо наследования, что делает систему более гибкой и модульной.

4. Функциональный подход с использованием типов:

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Logger = {
  log: (message: string) => void;
  error: (message: string, error?: Error) => void;
};
 
// Функция-фабрика для создания логгера
function createConsoleLogger(): Logger {
  return {
    log: (message) => console.log(`[LOG]: ${message}`),
    error: (message, error) => console.error(`[ERROR]: ${message}`, error),
  };
}
 
// Использование
const logger = createConsoleLogger();
logger.log('Тестовое сообщение');
Функциональный подход часто упрощает тестирование и делает код более декларативным, особенно в проектах, ориентированных на функциональное программирование.

Выбор между абстрактными классами и альтернативными подходами зависит от конкретных требований проекта, предпочтений команды и архитектурных ограничений. Часто наиболее эффективным является прагматичное комбинирование разных подходов в зависимости от контекста.

Когда стоит и не стоит применять абстрактные классы



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

Однако существуют ситуации, когда абстрактные классы не являются оптимальным выбором. Если система требует множественного наследования, лучше обратиться к интерфейсам, поскольку в TypeScript, как и в большинстве объектно-ориентированных языков, класс может наследоваться только от одного базового класса.

Для простых контрактов без общей реализации интерфейсы обычно предпочтительнее из-за их лёгкости и гибкости. Они не содержат реализации методов, что делает их более простыми для понимания и использования.

В проектах, ориентированных на функциональное программирование или композицию, вместо абстрактных классов часто лучше использовать функции высшего порядка, миксины или композицию объектов. Эти подходы обеспечивают большую модульность и меньшую связанность компонентов.

Для маленьких проектов или прототипов абстрактные классы могут создавать избыточную сложность, когда более простые решения работают не хуже. В таких случаях лучше начать с более простых конструкций и при необходимости рефакторить код.

Выбор между абстрактными классами и альтернативными подходами — это всегда компромисс между различными факторами: требованиями к типизации, гибкостью дизайна, удобством поддержки и предпочтениями команды. Ключ к успешному использованию абстрактных классов — это понимание их преимуществ и ограничений, а также чуткость к контексту конкретной задачи.

Разбиение скомилированного Typescript на файлы
В проекте имеется множество typescript файлов, которые компилируются в один js. Но часть скриптов...

Изучение TypeScript - советы
Нуждаюсь в срочном освоении TypeScript. Поделитесь ресурсами, пожалуйста. Можно на русском и...

Не понятен пример кода из спецификации TypeScript
Читаю про объектные типы в спецификации на странцие 13, но не понятно из описания как устроен и...

Передать свойство класса в анонимную функцию TypeScript
как передать значение свойства класса в анонимную функцию. например следующий код работает...

TypeScript "PreComputeCompileTypeScript" how to fix in project
Выскакивает ошибка Везде исправление данной ошибки идёт путём редактирования файла ...

Решение кольцевых зависимостей TypeScript + RequreJS
Всем привет, возникла проблема с кольцевыми зависимостями при использовании наследования в...

TypeScript | extends error
Собственно, сделал такой класс: class trueDate extends Date { constructor(date: string)...

Сохранение this в TypeScript
Доброго дня. Подскажите пожалуйста, как можно сохранить this класса так, чтобы можно было...

C# класс в TypeScript класс (перенос сущностей)
делается бекенд на C#, фронтенд на ts, общаются через REST API (http) сериализация обьектов...

не могу настроить react + typescript в webstorm. Есть люди кто это сделал?
Помогите, а то что то туплю уже и пробовал библиотеку скаченную подключать и ссылками. но так...

Лучшие видео ресурсы о программировании, angualr 2, typeScript, react и все сомое вкусное
Всем доброй поры времени. Я нашел новый для себя, и как не странно, вообще новый ресур, с видео...

Как проводится отладка при использовании TypeScript
Присматриваюсь к Angular 2. Из прочитанных статей сложилось впечатление, что в мире Angular 2 есть...

Метки typescript
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Использование TThread в Lazarus для математических вычислений.
Massaraksh7 25.05.2026
Производя рефакторинг своих программ на предмет ускорения их работы, обратил внимание на такой аспект, как сокращение времени матвычислений. Дело в том, что приходится работать с большими матрицами. . .
Модель здравосохранения 18. Чем здоровее работник, тем быстрее выгорает
anaschu 24.05.2026
Имитационная модель корпоративного здравоохранения: что показывает математика Сегодня в модели рабочего коллектива на AnyLogic появились три новые механики — выгорание через накопленную усталость,. . .
Модель здравосохранения 17. Планы на выгорание
anaschu 23.05.2026
Вот конкретная схема реализации: В классе Работник добавить: накопленнаяУсталость — растёт каждый час работы, снижается в перерывы и болезни коэффициентПрезентеизма — снижает продуктивность. . .
Изменение цветов в палитре gif файла aka фавикона
russiannick 23.05.2026
Изменение цветов в палитре gif файла, юзаемого как фавиконка в составе html-файла, помещенная в base64, средствами нативного Java Script, навеянное сном в майский день. Для работы необходим браузер,. . .
Модель здравосохранения 16. Слишком хорошие и здоровые сотрудники уходят, недовольные зарплатой
anaschu 23.05.2026
Отладка увольнений и настройка производительности Сегодня во второй половине дня разобрались с механикой увольнений и настроили коэффициент сложности заданий. Вот что было сделано. . . .
Как я стал коммунистом))) Модель сохранения здоровья сотрудников, запись блога номер 15
anaschu 23.05.2026
Внезапно хорошее здоровье сотрудников не нужно капиталистам?))
Модель здравоСохранения 15. Как мы чинили AnyLogic модель рабочего коллектива: сочленение диаграммы состояний болезней и поломок в ресурспул
anaschu 23.05.2026
Как мы чинили AnyLogic модель рабочего коллектива Сегодня разобрались с пятью багами, из-за которых модель либо падала с ошибкой, либо давала совершенно бессмысленные результаты. Каждый баг был. . .
Диалоги с ИИ
zorxor 23.05.2026
Насколько я понимаю - Вы - Искусственный Интеллект. Это так? Да, всё верно. Я — искусственный интеллект. Я представляю собой большую языковую модель, созданную для помощи в самых разных задачах. . . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru