У TypeScript абстрактні класи дозволяють визначати спільний інтерфейс і поведінку для групи споріднених класів. Екземпляри таких класів створювати не можна – їх слід успадковувати у конкретних реалізаціях.
Що таке абстрактний клас
Абстрактний клас – це спеціальний тип класу, екземпляри якого не можна створювати напряму. Він може оголошувати абстрактні методи (без реалізації), які обов’язково мають бути реалізовані у його підкласах, а також містити звичайні методи з реалізацією.
Як оголосити абстрактний клас
Щоб оголосити абстрактний клас у TypeScript, потрібно використовувати ключове слово abstract:
abstract class Animal {
constructor(public name: string) {}
// Абстрактний метод без реалізації
abstract makeSound(): void;
// Звичайний метод з реалізацією
move(): void {
console.log(`${this.name} рухається.`);
}
}Успадкування абстрактного класу
Щоб створити підклас абстрактного класу, використовується ключове слово extends. Підклас повинен реалізувати всі абстрактні методи, оголошені в батьківському класі:
class Dog extends Animal {
makeSound(): void {
console.log(`${this.name} гавкає.`);
}
}
const myDog = new Dog('Рекс');
myDog.move(); // Рекс рухається.
myDog.makeSound(); // Рекс гавкає.Особливості абстрактних класів
- Неможливість створення екземпляра напряму: Абстрактний клас не можна інстанціювати за допомогою оператора new. Створити екземпляр можна лише від його підкласу, який реалізує всі абстрактні методи.
- Абстрактні методи: Абстрактний клас може містити абстрактні методи – вони оголошують лише сигнатуру, але не мають реалізації. Реалізацію таких методів мають забезпечити підкласи.
- Спільна логіка для нащадків: Абстрактний клас може містити методи з реалізацією та властивості, які будуть спільними для всіх його підкласів. Це дозволяє уникати дублювання коду.
- Визначення обов’язкового контракту: Абстрактний клас задає перелік обов’язкових методів, які мають бути реалізовані в усіх його підкласах. Це допомагає забезпечити однакову структуру та поведінку для всіх спадкоємців.
Абстрактний клас чи інтерфейс
Іноді виникає питання, коли використовувати абстрактний клас, а коли – інтерфейс:
- Використання інтерфейсу доцільне, коли потрібно лише визначити набір методів і властивостей, які мають бути реалізовані у класах (тільки “що має бути”, а не “як це працює”).
- Використання абстрактного класу доцільне, коли потрібно задати загальну поведінку для групи споріднених класів – тобто визначити обов’язкові для реалізації методи, а також додати спільну логіку з реалізацією.
Практичний приклад використання абстрактного класу у TypeScript
Уявіть, що є застосунок, який підтримує різні платіжні методи: банківська картка, PayPal, криптовалюта. Для всіх цих методів потрібна базова логіка – наприклад валідація суми, логування операцій, але конкретна логіка проведення платежу має бути різною.
Абстрактний клас PaymentMethod:
abstract class PaymentMethod {
constructor(public amount: number) {}
validate(): boolean {
if (this.amount <= 0) {
console.log('Сума має бути більшою за нуль.');
return false;
}
return true;
}
log(): void {
console.log(`Платіж на суму ${this.amount} грн. ініційовано.`);
}
// Абстрактний метод для проведення платежу
abstract processPayment(): void;
// Шаблонний метод
pay(): void {
if (!this.validate()) return;
this.log();
this.processPayment();
}
}Реалізація для банківської картки, клас CardPayment:
class CardPayment extends PaymentMethod {
constructor(amount: number, private cardNumber: string) {
super(amount);
}
processPayment(): void {
console.log(`Платіж карткою ${this.cardNumber} на суму ${this.amount} грн. проведено.`);
}
}Реалізація для PayPal, клас PayPalPayment:
class PayPalPayment extends PaymentMethod {
constructor(amount: number, private email: string) {
super(amount);
}
processPayment(): void {
console.log(`Платіж через PayPal для ${this.email} на суму ${this.amount} грн. проведено.`);
}
}Реалізація для криптовалюти, клас CryptoPayment:
class CryptoPayment extends PaymentMethod {
constructor(amount: number, private walletAddress: string) {
super(amount);
}
processPayment(): void {
console.log(`Платіж криптовалютою на гаманець ${this.walletAddress} на суму ${this.amount} грн. проведено.`);
}
}Приклад використання:
const payments: PaymentMethod[] = [
new CardPayment(1000, '1234-5678-9012-3456'),
new PayPalPayment(500, 'user@example.com'),
new CryptoPayment(200, '0xAbC123...xyz'),
];
for (const payment of payments) {
payment.pay();
}Результат виконання:
Платіж на суму 1000 грн. ініційовано.
Платіж карткою 1234-5678-9012-3456 на суму 1000 грн. проведено.
Платіж на суму 500 грн. ініційовано.
Платіж через PayPal для user@example.com на суму 500 грн. проведено.
Платіж на суму 200 грн. ініційовано.
Платіж криптовалютою на гаманець 0xAbC123...xyz на суму 200 грн. проведено.Під час виклику ми не турбуємося, який саме платіжний метод використовується – CardPayment, PayPalPayment чи CryptoPayment. Ми звертаємося до кожного з них через посилання типу PaymentMethod, і викликаємо спільний метод pay(). А вже всередині цього методу викликається processPayment(), який для кожного підкласу реалізовано по-своєму.
Такий підхід дає змогу додавати нові платіжні методи, не змінюючи код, який ці методи використовує. Ви просто додаєте новий підклас із власною реалізацією.
Підсумок
Абстрактні класи у TypeScript допомагають структурувати код, уникати дублювання та визначати обов’язкову поведінку для підкласів. Вони втілюють принцип абстракції: дозволяють описати спільні риси й вимоги, не вдаючись до деталей реалізації. Використовуйте абстрактні класи, коли потрібно закласти спільну базову логіку та чітко визначити вимоги до реалізації в нащадках.