Об’єктно-орієнтоване програмування (ООП) – це парадигма програмування, яка базується на концепції об’єктів. Об’єкти – це екземпляри класів, що поєднують у собі дані (властивості) та поведінку (методи). Основні принципи ООП:

  • Інкапсуляція
  • Наслідування
  • Поліморфізм
  • Абстракція

У цьому дописі буде розглянуто основи реалізації ООП у TypeScript.

Класи та об’єкти

У TypeScript клас – це шаблон для створення об’єктів. Він визначає властивості та поведінку, які матимуть об’єкти цього класу. Ми можемо створити безліч екземплярів класу (об’єктів), кожен з яких має власний стан (властивості) та спільну поведінку (методи).

Розглянемо приклад класу Person у TypeScript:

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): void {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

// Створення екземплярів класу Person
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.greet(); // Hello, my name is Alice and I am 30 years old.
person2.greet(); // Hello, my name is Bob and I am 25 years old.

Пояснення:

  • class Person – оголошення класу
  • name та age – властивості класу
  • constructor – спеціальний метод, який викликається під час створення об’єкта та ініціалізує властивості
  • greet() – метод класу, який описує поведінку
  • person1, person2 – екземпляри (об’єкти) класу з власним станом

Цей приклад демонструє, яким чином клас задає структуру для майбутніх об’єктів, а також як створювати та використовувати ці об’єкти у TypeScript.

Інкапсуляція та модифікатори доступу

Інкапсуляція – це принцип ООП, який дозволяє об’єднувати дані та методи в межах класу, приховуючи внутрішні деталі реалізації від зовнішнього світу.

У TypeScript інкапсуляція реалізується за допомогою модифікаторів доступу:

  • public – модифікатор за замовчуванням. Такі члени класу доступні з будь-якого місця.
  • private – приватні члени доступні лише в межах класу, де вони оголошені.
  • protected – захищені члени доступні в класі, що їх визначає, та його підкласах.
  • readonly – властивість доступна тільки для читання (присвоїти значення можна лише під час ініціалізації).

Приклад:

class Employee {
  public name: string; // Доступно з будь-якого місця
  private salary: number; // Доступно тільки в класі Employee
  protected department: string; // Доступно в Employee і його підкласах
  public readonly id: number; // Доступно ззовні, але лише для читання

  constructor(name: string, salary: number, department: string, id: number) {
    this.name = name;
    this.salary = salary;
    this.department = department;
    this.id = id;
  }

  public getInfo(): string {
    return `Name: ${this.name}, Department: ${this.department}, ID: ${this.id}`;
  }

  public updateSalary(newSalary: number): void {
    this.salary = newSalary; // salary можна змінити лише в середині класу
  }

  public getSalary(): number {
    return this.salary;
  }
}

class Manager extends Employee {
  public manage(): void {
    this.salary; // Error: Property 'salary' is private and only accessible within class 'Employee'
    console.log(`Managing department: ${this.department}`); // OK: protected
  }
}

const alice = new Manager('Alice', 5000, 'HR', 1001);

console.log(alice.name); // OK (public)
console.log(alice.getInfo()); // OK (public)
console.log(alice.id); // OK (readonly)
alice.id = 2002; // Error: Cannot assign to 'id' because it is a read-only property
console.log(alice.salary); // Error: Property 'salary' is private and only accessible within class 'Employee'
console.log(alice.department); // Error: Property 'department' is protected and only accessible within class 'Employee' and its subclasses
alice.manage(); // OK

Наслідування (Inheritance)

Наслідування – це механізм ООП, який дозволяє створювати нові класи на основі вже існуючих. Дочірній клас отримує всі властивості й методи батьківського класу, а також може додавати власні або перевизначати успадковані. Це сприяє повторному використанню коду та допомагає створювати більш спеціалізовані класи на основі існуючих.

TypeScript підтримує одиночне (single) успадкування: клас може наслідувати лише один базовий клас.

Приклад:

// Базовий клас
class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a sound.`);
  }
}

// Дочірній клас, який наслідує Animal
class Dog extends Animal {
  constructor(name: string) {
    super(name); // Викликає конструктор батьківського класу
  }

  // Перевизначаємо метод speak()
  speak(): void {
    console.log(`${this.name} barks.`);
  }

  fetch(): void {
    console.log(`${this.name} is fetching a ball.`);
  }
}

// Створення екземпляра дочірнього класу
const rex = new Dog('Rex');

rex.speak(); // Rex barks.
rex.fetch(); // Rex is fetching a ball.

Пояснення:

  • Dog наслідує всі властивості та методи класу Animal
  • метод speak() перевизначено у класі Dog
  • додано новий метод fetch(), який є тільки у класі Dog
  • об’єкт rex демонструє використання обох методів

Поліморфізм (Polymorphism)

Поліморфізм (від грец. «багато форм») – це принцип ООП, який дозволяє викликати один і той самий метод у різних об’єктів, а виконання залежить від конкретного класу об’єкта.

Ключова ідея полягає в тому, що ви можете працювати з різними об’єктами через спільний тип (базовий клас, абстрактний клас або інтерфейс), не знаючи їхнього конкретного типу під час написання коду. Простими словами, ви створюєте єдиний «контракт» (інтерфейс), а різні об’єкти реалізують цей контракт по-різному. Це дозволяє вашому коду бути гнучким і розширюваним.

У TypeScript поліморфізм реалізується через наслідування або реалізацію спільного інтерфейсу. Розглянемо обидва випадки.

Поліморфізм через базовий клас:

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak(): void {
    console.log(`${this.name} barks.`);
  }
}

class Cat extends Animal {
  speak(): void {
    console.log(`${this.name} meows.`);
  }
}

// Масив з різних тварин, тип Animal
const animals: Animal[] = [new Dog('Rex'), new Cat('Barsik'), new Dog('Buddy')];

// Всі об’єкти реагують на метод speak(), але кожен по-своєму
for (const animal of animals) {
  animal.speak();
}

// Вивід:
// Rex barks.
// Barsik meows.
// Buddy barks.

Поліморфізм через інтерфейс:

interface Shape {
  getArea(): number;
}

class Circle implements Shape {
  constructor(public radius: number) {}

  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle implements Shape {
  constructor(public width: number, public height: number) {}

  getArea(): number {
    return this.width * this.height;
  }
}

const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];

for (const shape of shapes) {
  console.log(shape.getArea());
}

// Вивід:
// 78.53981633974483
// 24

Поліморфізм дозволяє використовувати спільний тип (Animal або Shape) для масиву об’єктів, не турбуючись про конкретний клас. Кожен об’єкт реагує на виклик методу відповідно до своєї реалізації, що робить код гнучкішим і розширюваним.

Абстракція (Abstraction)

Абстракція – це принцип ООП, який дозволяє представляти складні об’єкти у спрощеному та узагальненому вигляді. Вона фокусується на визначенні основних характеристик і поведінки об’єкта, приховуючи другорядні деталі. У TypeScript абстракція досягається за допомогою абстрактних класів та інтерфейсів.

Абстрактний клас – це шаблон для інших класів, його екземпляр не можна створити безпосередньо. Він може містити абстрактні методи (без реалізації) та конкретні методи (з реалізацією). Підкласи, що наслідують абстрактний клас, зобов’язані реалізувати всі абстрактні методи:

// Абстрактний клас
abstract class Vehicle {
  abstract move(): void; // абстрактний метод, без реалізації

  start(): void {
    // конкретний метод, з реалізацією
    console.log('Engine started');
  }
}

// Підклас, який наслідує Vehicle
class Car extends Vehicle {
  move(): void {
    // обов'язкова реалізація абстрактного методу
    console.log('The car is moving');
  }
}

// Ще один підклас
class Bike extends Vehicle {
  move(): void {
    console.log('The bike is moving');
  }
}

// Неможливо створити екземпляр абстрактного класу напряму
const vehicle = new Vehicle(); // Error: Cannot create an instance of an abstract class

const car = new Car();
const bike = new Bike();

car.start(); // Engine started
car.move(); // The car is moving

bike.start(); // Engine started
bike.move(); // The bike is moving

Інтерфейс – це контракт, який визначає структуру об’єкта: властивості та методи, які клас повинен реалізувати.

Інтерфейси в TypeScript дозволяють класу реалізовувати кілька контрактів одночасно. Це часто називають «множинним успадкуванням через інтерфейси», але насправді йдеться лише про комбінування структур, без успадкування реалізації:

// Оголошення інтерфейсів
interface Flyer {
  fly(): void;
}

interface Swimmer {
  swim(): void;
}

// Клас реалізує два інтерфейси одночасно (множинне успадкування через інтерфейси)
class Duck implements Flyer, Swimmer {
  fly(): void {
    console.log('Duck is flying');
  }

  swim(): void {
    console.log('Duck is swimming');
  }
}

const donald = new Duck();
donald.fly(); // Duck is flying
donald.swim(); // Duck is swimming

Пояснення:

  • Інтерфейси Flyer та Swimmer описують методи, які має реалізувати клас.
  • Клас Duck реалізує обидва інтерфейси, беручи на себе зобов’язання надати реалізацію методів з обох інтерфейсів.
  • Таке множинне успадкування можливе лише через інтерфейси – через класи у TypeScript підтримується тільки одиночне наслідування.

Підсумок

Об’єктно-орієнтоване програмування (ООП) – це підхід, який допомагає створювати структуровані, масштабовані та легко підтримувані додатки.

TypeScript повністю підтримує ключові концепції ООП: класи, об’єкти, інкапсуляцію, наслідування, поліморфізм та абстракцію. Завдяки цьому ви можете будувати код, який простіше розширювати, тестувати й адаптувати під нові вимоги.


Мітки: