Сегодня мы разберем одну из фундаментальных и элегантных концепций объектно-ориентированного программирования (ООП) это абстракцию, а конкретно ее реализацию через абстрактные классы и методы.
Если раньше мы говорили о том, как что-то делать (инкапсуляция, наследование, полиморфизм), то сегодня поговорим о том, что должно быть сделано, не вдаваясь в детали реализации. Это подход архитектора, который рисует план здания, а не прораба, который руководит кладкой кирпичей.
Концепция абстракции в ООП
Прежде чем мы погрузимся в синтаксис TypeScript, давай четко поймем, что такое абстракция. Это не просто какое-то техническое слово, это способ мышления.
Абстракция это процесс сокрытия сложной реализации и предоставления пользователю только самого необходимого, упрощенного интерфейса для работы. Мы абстрагируемся от деталей каждый день. Когда ты поворачиваешь руль автомобиля, ты не думаешь о том, как через рулевой механизм усилие передается на колеса. Ты просто используешь интерфейс (руль) для достижения цели: повернуть направо или налево. Руль это абстракция над сложной системой управления автомобилем.
В программировании абстракция позволяет нам определить что должен делать объект, не определяя как он это делает на самом начальном этапе проектирования. Мы создаем некий каркас, шаблон, контракт, который затем будут реализовывать другие, более конкретные классы. Это помогает управлять сложностью больших проектов, разбивая их на понятные, логические части с четко определенными ролями.
В TypeScript абстракция primarily достигается двумя способами, с помощью интерфейсов (которые мы уже подробно разбирали) и с помощью абстрактных классов, которым и посвящен этот урок. Абстрактные классы это often более мощный инструмент, так как они могут содержать не только сигнатуры методов (как интерфейсы), но и их реализацию, а также свойства.
Ключевое слово abstract
TypeScript, как язык, расширяющий возможности JavaScript, добавляет ключевое слово abstract. Оно используется исключительно для обозначения абстрактных классов и абстрактных методов. Это своего рода метка для компилятора: «Эй, этот класс или метод не предназначен для прямого использования, он лишь чертеж для будущих реализаций».
Ключевое слово abstract ставится перед объявлением класса или метода внутри абстрактного класса.
-
Абстрактный класс объявляется как
abstract class ClassName {}. -
Абстрактный метод объявляется как
abstract methodName(): returnType;.
Самое главное правило: нельзя создать экземпляр (инстанс) абстрактного класса напрямую с помощью оператора new. Попытка сделать это приведет к ошибке компиляции.
// Объявляем абстрактный класс abstract class Vehicle { // Обычное свойство с реализацией protected brand: string; constructor(brand: string) { this.brand = brand; } // Абстрактный метод - без реализации abstract startEngine(): void; // Обычный метод с реализацией honk(): void { console.log('Beep beep!'); } } // Попытка создать экземпляр абстрактного класса - ОШИБКА! // const myVehicle = new Vehicle('Tesla'); // Error: Cannot create an instance of an abstract class. // Это не скомпилируется.
Зачем тогда нужен такой класс, который нельзя использовать? Его предназначение быть родительским классом, от которого другие, неабстрактные (конкретные) классы, будут наследовать структуру и, возможно, часть реализации. Абстрактный класс обязывает свои дочерние классы предоставить конкретную реализацию для всех объявленных в нем абстрактных методов.
Чем абстрактный класс отличается от обычного
Давай проведем четкую грань между двумя этими понятиями. Понимание этих различий, является ключом к правильному применению абстракции.
-
Возможность создания экземпляра.
-
Обычный класс: Предназначен для создания экземпляров. Мы используем
new ClassName()постоянно. -
Абстрактный класс: Не может быть instantiated. Он существует только для того, чтобы от него наследовались. Он концепция, идея, а не конкретная сущность.
-
-
Наличие абстрактных методов.
-
Обычный класс: Не может содержать абстрактных методов. Все его методы должны иметь тело (реализацию).
-
Абстрактный класс: Может (и обычно так и делает) содержать абстрактные методы, методы без реализации. Это его главная distinguishing feature.
-
-
Цель использования.
-
Обычный класс: Моделирует конкретную сущность, с которой можно работать напрямую (
new User(),new Product()). -
Абстрактный класс: Служит базовым классом для группы родственных классов. Он определяет общий интерфейс (в широком смысле) и общую функциональность для них. Например, абстрактный класс
Animalс абстрактным методомmakeSound(). КлассыDog,CatиLionнаследуют отAnimalи предоставляют свою реализациюmakeSound().
-
-
Степень завершенности.
-
Обычный класс: Полностью завершенная и готовая к использованию единица.
-
Абстрактный класс: Частично завершен. Он может содержать как готовые, реализованные методы, так и «заглушки» (абстрактные методы), которые должны быть «доделаны» в дочерних классах.
-
Проще говоря, обычный класс это готовый продукт (например, купленный в магазине стол). Абстрактный класс это подробный чертеж и набор готовых деталей для сборки этого стола (ножки, столешница), но с требованием самостоятельно сделать и прикрутить фигурную царгу (абстрактный метод). Ты не можешь использовать чертеж как стол, но можешь по нему собрать много разных столов, гарантированно получив одинаковый интерфейс (у всех будет царга, даже если она разной формы).
Создание и использование абстрактных классов
Давай рассмотрим практический пример, который объединит все сказанное выше. Представим, что мы проектируем систему для геометрических фигур.
// Абстрактный класс, определяющий общую концепцию "Фигура" abstract class Shape { // Обычное свойство с реализацией. Цвет есть у любой фигуры. protected color: string; constructor(color: string) { this.color = color; } // Абстрактный метод. У каждой фигуры есть площадь, но алгоритм расчета разный. // Мы ОБЯЗЫВАЕМ дочерние классы реализовать этот метод. abstract calculateArea(): number; // Абстрактный метод для периметра/длины. abstract calculatePerimeter(): number; // Обычный метод с реализацией. Описать фигуру можно одинаково для всех, используя абстрактные методы. // Это и есть красота полиморфизма и абстракции! describe(): void { console.log(`This is a ${this.color} shape with an area of ${this.calculateArea()} and perimeter of ${this.calculatePerimeter()}.`); } }
Теперь мы не можем создать просто «Фигуру». Это бессмысленно. Но мы можем создать конкретные фигуры, которые наследуют от этого абстрактного класса и предоставляют реализацию всех абстрактных методов.
// Конкретный класс Круг class Circle extends Shape { private radius: number; constructor(color: string, radius: number) { super(color); // вызываем конструктор родительского класса this.radius = radius; } // Реализуем абстрактный метод calculateArea для круга calculateArea(): number { return Math.PI * this.radius * this.radius; } // Реализуем абстрактный метод calculatePerimeter для круга calculatePerimeter(): number { return 2 * Math.PI * this.radius; } } // Конкретный класс Прямоугольник class Rectangle extends Shape { private width: number; private height: number; constructor(color: string, width: number, height: number) { super(color); this.width = width; this.height = height; } calculateArea(): number { return this.width * this.height; } calculatePerimeter(): number { return 2 * (this.width + this.height); } }
Теперь давай посмотрим, как это все работает вместе и в чем суть полиморфизма:
// Создаем экземпляры КОНКРЕТНЫХ классов const myCircle = new Circle('red', 5); const myRectangle = new Rectangle('blue', 4, 6); // Вызываем метод describe(), который определен в абстрактном классе myCircle.describe(); // "This is a red shape with an area of 78.5398... and perimeter of 31.4159..." myRectangle.describe(); // "This is a blue shape with an area of 24 and perimeter of 20." // Мы можем работать с фигурами через тип их общего предка const shapes: Shape[] = [myCircle, myRectangle]; for (const shape of shapes) { shape.describe(); // Для каждого элемента массива вызовется своя реализация calculateArea и calculatePerimeter! // Это и есть полиморфизм во всей красе. }
Обрати внимание: метод describe определен единожды в абстрактном классе Shape, но он использует абстрактные методы calculateArea и calculatePerimeter. Когда мы вызываем describe у экземпляра Circle, он использует реализацию calculateArea из класса Circle. Когда мы вызываем его у Rectangle из класса Rectangle. Абстрактный класс предоставляет общий шаблон поведения, а конкретные классы наполняют его жизнью.
Абстрактные свойства и access modifiers
Абстрактными могут быть не только методы, но и свойства. Это позволяет узнать наличие определенных свойств в дочерних классах.
abstract class DatabaseConnector { // Абстрактное свойство. Дочерние классы ДОЛЖНЫ его объявить. abstract databaseName: string; // Абстрактное свойство только для чтения abstract readonly connectionProtocol: string; // Обычный метод, который может использовать абстрактные свойства connect(): void { console.log(`Connecting to ${this.databaseName} via ${this.connectionProtocol}...`); } abstract query(sql: string): unknown[]; } class MySqlConnector extends DatabaseConnector { // Реализуем абстрактные свойства databaseName: string; readonly connectionProtocol: string = 'TCP/IP'; // Можно инициализировать сразу constructor(dbName: string) { super(); this.databaseName = dbName; } query(sql: string): unknown[] { // ... логика запроса для MySQL ... console.log(`Executing MySQL query: ${sql}`); return []; } } const mysql = new MySqlConnector('my-db'); mysql.connect(); // "Connecting to my-db via TCP/IP..."
Что касается модификаторов доступа (public, protected, private), они работают с абстрактными членами класса стандартным образом.
-
public abstract. Член должен быть реализован как публичный в дочернем классе. -
protected abstract. Член должен быть реализован как защищенный в дочернем классе. Это наиболее распространенный вариант для абстрактных методов, так как они часто являются частью внутренней логики класса, а не его публичного API.
Использование private abstract запрещено, потому что дочерний класс не смог бы реализовать приватный член родительского класса, он просто не имеет к нему доступа.
Абстрактные классы или интерфейсы
Это очень частый и правильный вопрос. Оба инструмента позволяют определять контракты, но делают это по-разному.
| Характеристика | Абстрактный Класс | Интерфейс |
|---|---|---|
| Реализация | Может содержать как реализацию методов, так и абстрактные методы. | Может содержать только объявления методов и свойств (до версии TS 4.2+). |
| Свойства | Может содержать реализованные и абстрактные свойства. | Может только объявлять свойства. |
| Конструктор | Может иметь конструктор. | Не может иметь конструктор. |
| Модификаторы | Поддерживает public, protected, private. |
Все члены по умолчанию и всегда public. |
| Наследование | Класс может наследовать только от одного абстрактного класса. | Класс может реализовывать множество интерфейсов. |
| Цель | Создание базового класса для родственных объектов, предоставление общей функциональности. | Определение контракта для любых объектов, несвязанных наследованием. Описание формы объекта. |
Когда использовать абстрактный класс?
-
Когда несколько родственных классов должны использовать общую логику (реализованные методы).
-
Когда вам нужно определить общий шаблон с обязательными для реализации частями (абстрактными методами).
-
Когда вы хотите использовать модификаторы доступа, отличные от
public.
Когда использовать интерфейс?
-
Когда вам нужно описать контракт, который могут реализовать даже совершенно несвязанные классы.
-
Когда вам нужна возможность множественного «наследования» (реализации нескольких интерфейсов).
-
Когда вы описываете форму объекта для внешнего API.
Часто они используются вместе, абстрактный класс реализует интерфейс и предоставляет часть стандартной реализации для него.
interface Logger { log(message: string): void; } // Абстрактный класс предоставляет частичную реализацию интерфейса abstract class BaseLogger implements Logger { abstract log(message: string): void; // Все равно forcing дочерние классы к реализации // Но добавляет общую функциональность protected getTimestamp(): string { return new Date().toISOString(); } } class ConsoleLogger extends BaseLogger { log(message: string): void { console.log(`[${this.getTimestamp()}] ${message}`); // Используем метод из абстрактного класса } }
Практические задачи для закрепления
Давай закрепим знания на реальных задачах.
Задача 1: Система оплаты
Создай абстрактный класс PaymentProcessor с абстрактными методами processPayment(amount: number): boolean и refundPayment(transactionId: string): boolean. Создай два конкретных класса: CreditCardProcessor и PayPalProcessor, которые реализуют эти методы (просто выводи в консоль сообщение о том, какой метод и для какой системы вызван и возвращай true). Добавь в абстрактный класс общий метод getPaymentDetails(transactionId: string): void, который выводит сообщение «Fetching details for transaction #…».
Решение (напиши свое сначала сам!):
abstract class PaymentProcessor { abstract processPayment(amount: number): boolean; abstract refundPayment(transactionId: string): boolean; getPaymentDetails(transactionId: string): void { console.log(`Fetching details for transaction #${transactionId}...`); } } class CreditCardProcessor extends PaymentProcessor { processPayment(amount: number): boolean { console.log(`Processing credit card payment of $${amount}`); return true; } refundPayment(transactionId: string): boolean { console.log(`Refunding credit card payment for transaction #${transactionId}`); return true; } } class PayPalProcessor extends PaymentProcessor { processPayment(amount: number): boolean { console.log(`Processing PayPal payment of $${amount}`); return true; } refundPayment(transactionId: string): boolean { console.log(`Refunding PayPal payment for transaction #${transactionId}`); return true; } } // Использование: const cardProcessor = new CreditCardProcessor(); cardProcessor.processPayment(100); // "Processing credit card payment of $100" cardProcessor.getPaymentDetails('abc-123'); // "Fetching details for transaction #abc-123..." const paypalProcessor = new PayPalProcessor(); paypalProcessor.refundPayment('def-456'); // "Refunding PayPal payment for transaction #def-456"
Задача 2: Животные (продолжение)
Разработай иерархию классов. Абстрактный класс Animal должен иметь свойства name: string (инициализируется в конструкторе) и abstract sound: string (звук, который издает животное). Добавь абстрактный метод makeSound(): void, который должен выводить в консоль строку ${name} says: ${sound}!. Затем создай классы Dog (sound = 'Woof'), Cat (sound = 'Meow') и Cow (sound = 'Moo'). Подумай, где лучше реализовать логику вывода звука: в абстрактном методе makeSound или в каждом дочернем классе? Можно ли это сделать так, чтобы в дочерних классах не пришлось реализовывать makeSound, а только задавать sound?
Решение (подход с абстрактным геттером):
abstract class Animal { constructor(public name: string) {} // Публичное свойство инициализируется через конструктор // Абстрактное свойство (геттер). Дочерние классы должны его реализовать. abstract get sound(): string; // НЕ абстрактный метод! Он использует абстрактное свойство sound. // Логика едина для всех животных, поэтому ее выносим в базовый класс. makeSound(): void { console.log(`${this.name} says: ${this.sound}!`); } } class Dog extends Animal { get sound(): string { // Реализуем абстрактный геттер return 'Woof'; } } class Cat extends Animal { get sound(): string { return 'Meow'; } } class Cow extends Animal { get sound(): string { return 'Moo'; } } const animals: Animal[] = [ new Dog('Rex'), new Cat('Whiskers'), new Cow('Bessie') ]; for (const animal of animals) { animal.makeSound(); // У каждого будет свой звук! } // Rex says: Woof! // Whiskers says: Meow! // Bessie says: Moo!
Этот подход демонстрирует всю мощь абстракции: общая логика (makeSound) описана в одном месте, а уникальные данные (sound) предоставляются каждым конкретным классом.
Заключение
Абстрактные классы это прекрасный инструмент для проектирования архитектуры вашего приложения. Они позволяют создавать строгие, предсказуемые иерархии классов, обеспечивая выполнение контракта и одновременно позволяя переиспользовать общий код. Они находятся на стыке наследования и полиморфизма, делая вашу кодобазу более гибкой, понятной и легкой для поддержки.
Абстрактный класс это скелет будущих объектов. Он диктует структуру, но позволяет «плоти» конкретных реализаций варьироваться. Используй эту силу с умом, не создавай абстрактные классы «на всякий случай», а применяй их там, где есть четкая, ясная иерархия и общая логика для группы объектов.
Это был непростой, но очень важный урок. Обязательно попрактикуйся, написав свои собственные абстрактные иерархии.
Полный курс с уроками по TypeScript для начинающих можно найти по ссылке: https://max-gabov.ru/typescript-dlya-nachinaushih
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


