Спрощення концепцій об’єктно-орієнтованого програмування (OOP) за допомогою реальних прикладів на TS

pic

Що таке OOP?

Об'єктно-орієнтоване програмування (OOP) — це парадигма програмування (стиль, що використовується для написання та організації коду), яка організує та моделює програмне забезпечення за допомогою об'єктів. Вона сприяє повторному використанню коду, модульності та масштабованості.

Парадигми програмування:

1. Процедурне програмування:
Воно слідує покроковому підходу з використанням функцій і процедур для вирішення проблем. Код виконується в лінійному потоці.

Перевага: Просте і зрозуміле для малих програм, але важче керувати при зростанні складності.

# Мова C
void greet() {
 printf("Hello!");
}
int main() {
 greet();
 return 0;
}

2. Функціональне програмування:
Це програмування, яке зосереджене на чистих функціях і униканні змін даних або стану. Функції розглядаються як першокласні громадяни.

Перевага: Підвищує передбачуваність і тестованість коду, уникнувши побічних ефектів.

# Javascript
 const add = (a, b) => a + b;
console.log(add(2, 3)); // Вивід: 5

3. Декларативне програмування:
Це програмування, яке зосереджується на що потрібно зробити, а не як це зробити. Логіка обробляється підсистемою.

Перевага: Спрощує складні операції, абстрагуючи логіку, що робить код більш читабельним.

# SQL
 SELECT name FROM users WHERE age > 18;

4. Об'єктно-орієнтоване програмування (OOP):
Це програмування, яке організовує код в об'єкти з властивостями та методами для моделювання реальних сутностей. Воно сприяє повторному використанню коду та модульності.

Перевага: Спрощує складні системи через абстракцію і полегшує обслуговування за допомогою модульного коду.

# JS
class Animal {
 speak() {
 console.log("Тварина видає звук");
 }
}
const dog = new Animal();
dog.speak();

5. Подієво-орієнтоване програмування:
Програма реагує на дії користувача або системні події через обробники подій. Це поширене в GUI і веб-розробці.

Перевага: Покращує взаємодію з користувачем, реагуючи динамічно на події.

# JS
 document.getElementById("btn").onclick = () => alert("Кнопка натиснута!");

Об'єктно-орієнтоване програмування (OOP):

1. Наслідування

Наслідування дозволяє класу (дочірньому/підкласу) отримувати властивості та методи від іншого класу (батьківського/суперкласу). Це допомагає повторно використовувати код і розширювати функціональність.

Перевага: Сприяє повторному використанню коду та спрощує обслуговування коду.

# Батьківський клас: Person
class Person {
 constructor(public name: string, public age: number) {}
 introduce() {
 console.log(`Привіт, я ${this.name}, і мені ${this.age} років.`);
 }
}
# Дочірній клас: Student наслідує від Person
class Student extends Person {
 constructor(name: string, age: number, public grade: string) {
 super(name, age); // Викликає конструктор Person для встановлення name і age
 }
 study() {
 console.log(`${this.name} вчиться в класі ${this.grade}.`);
 }
}
# Створення екземпляра Student
const student1 = new Student("Аліса", 20, "А");
student1.introduce(); // Наслідуваний метод → Вивід: Привіт, я Аліса, і мені 20 років.
student1.study(); // Власний метод дочірнього класу → Вивід: Аліса вчиться в класі А.

2. Поліморфізм

Поліморфізм дозволяє тому самому методу вести себе по-різному в залежності від об'єкта, який його викликає. Це допомагає написати гнучкий і багаторазовий код, використовуючи один метод для багатьох типів об'єктів.

Перевага: Підвищує гнучкість і масштабованість, дозволяючи одному інтерфейсу підтримувати різні типи даних.

class Person {
 speak(): void {
 console.log("Особистість говорить.");
 }
}
class Student extends Person {
 speak(): void {
 console.log("Студент вчиться.");
 }
}
class Teacher extends Person {
 speak(): void {
 console.log("Вчитель навчає.");
 }
}
function introduce(person: Person): void {
 person.speak();
}
const student = new Student();
const teacher = new Teacher();
introduce(student); // Вивід: Студент вчиться.

introduce(teacher); // Вивід: Вчитель навчає.

3. Абстракція:

Абстракція в OOP приховує складні деталі реалізації і показує лише суттєві функції для користувача. Це спрощує код, зосереджуючи увагу на тому, що об'єкт робить, а не на тому, як він це робить.

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

abstract class Animal {  
 abstract makeSound(): void; // Абстрактний метод (без реалізації)  
 sleep(): void {  
 console.log("Спить…");  
 }  
}  
class Dog extends Animal {  
 makeSound(): void {  
 console.log("Гав!");  
 }  
}  
const dog = new Dog();  
dog.makeSound(); // Вивід: Гав!  
dog.sleep(); // Вивід: Спить…

4. Інкапсуляція:

Інкапсуляція в OOP — це процес об'єднання даних (властивостей) і методів (функцій) в єдину одиницю (клас) і обмеження прямого доступу до деяких компонентів об'єкта. Це захищає внутрішній стан об'єкта і дозволяє взаємодіяти з ним тільки через публічні методи.

Перевага: Захищає цілісність об'єкта, контролюючи, як дані доступаються і змінюються.

class Person {  
 private age: number; // Приватна властивість  
 constructor(age: number) {  
 this.age = age;  
 }  
 // Публічний метод для доступу до приватної властивості  
 getAge(): number {  
 return this.age;  
 }  
 // Публічний метод для зміни приватної властивості з умовою  
 setAge(newAge: number): void {  
 if (newAge > 0) {  
 this.age = newAge;  
 }  
 }  
}  
const person = new Person(25);  
console.log(person.getAge()); // Вивід: 25  
person.setAge(30);  
console.log(person.getAge()); // Вивід: 30  
// person.age = 40; // ❌ Помилка: Не можна доступитись до приватної властивості

Вступ до принципів OOP (SOLID):

  • S: Принцип єдиного обов'язку (Single Responsibility Principle) — Клас повинен мати лише одну відповідальність.

Перевага: Покращує обслуговуваність коду та зменшує складність, розділяючи обов'язки.
Приклад: Клас User повинен лише обробляти дані користувача, а не аутентифікацію.

class User {  
 constructor(public name: string, public email: string) {}  
}  
class AuthService {  
 login(user: User) {  
 console.log(`Логін користувача ${user.name}`);  
 }  
}
  • O: Принцип відкритості/закритості (Open/Closed Principle) — Класи повинні бути відкритими для розширення, але закритими для модифікації.

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

interface PaymentMethod {  
 pay(amount: number): void;  
}  
class CreditCard implements PaymentMethod {  
 pay(amount: number): void {  
 console.log(`Оплачено ${amount} за допомогою кредитної картки.`);  
 }  
}
  • L: Принцип підміни Ліскова (Liskov Substitution Principle) — Підкласи повинні замінювати базові класи без порушення функціональності.

Перевага: Забезпечує надійність при розширенні класів, запобігаючи непередбачуваній поведінці.
Приклад: Заміна базового класу Bird на Sparrow без проблем.

class Bird {  
 fly(): void {  
 console.log("Летить");  
 }  
}  
class Sparrow extends Bird {}  
const bird: Bird = new Sparrow();  
bird.fly();
  • I: Принцип сегрегації інтерфейсів (Interface Segregation Principle) — Уникайте примушення класів реалізовувати невикористовувані методи.

Перевага: Спрощує проектування класів і запобігає непотрібним залежностям.
Приклад: Розділення інтерфейсів для різних ролей користувачів.

interface Printer {  
 print(): void;  
}  
interface Scanner {  
 scan(): void;  
}  
class AllInOnePrinter implements Printer, Scanner {  
 print(): void {  
 console.log("Друк документа");  
 }  
 scan(): void {  
 console.log("Сканування документа");  
 }  
}
  • D: Принцип інверсії залежностей (Dependency Inversion Principle) — Модулі високого рівня повинні залежати від абстракцій, а не від конкретних реалізацій.

Перевага: Сприяє гнучкості та полегшує рефакторинг і обслуговування систем.
Приклад: Використання інтерфейсів замість прямої залежності від класів.

interface Database {  
 connect(): void;  
}  
class MySQL implements Database {  
 connect(): void {  
 console.log("Підключено до MySQL");  
 }  
}  
class App {  
 constructor(private db: Database) {}  
 start() {  
 this.db.connect();  
 }  
}  
const app = new App(new MySQL());  
app.start();

Важливі теми:

Клас з параметричними властивостями:

В OOP клас є шаблоном або кресленням для створення об'єктів. Він визначає властивості (атрибути) та поведінку (методи), які будуть мати об'єкти, створені з цього класу.

// Без параметричних властивостей  
class PersonWithoutPP {  
 private name: string; // Не можна змінити ззовні  
 private age: number;  
 constructor(name: string, age: number) {  
 this.name = name; // Ручне призначення  
 this.age = age;  
 }  
 greet(): void {  
 console.log(`Привіт, мене звуть ${this.name}.`);  
 }  
}  
const personA = new PersonWithoutPP("Аліса", 25);  
personA.greet(); // Вивід: Привіт, мене звуть Аліса  
// З параметричними властивостями (спрощено)  
class PersonWithPP {  
 constructor(private name: string, private age: number) {} // Автоматичне призначення  
 greet(): void {  
 console.log(`Привіт, мене звуть ${this.name}`);  
 }  
}  
const personB = new PersonWithPP("Боб", 30);  
personB.greet(); // Вивід: Привіт, мене звуть Боб

Тип guard typeof та in:

Тип guard typeof використовується для уточнення типу змінної залежно від її типу.

function greet(person: string | number) {  
 if (typeof person === "string") {  
 console.log(`Привіт, ${person}!`); // Якщо person — це рядок  
 } else {  
 console.log(`Привіт, число ${person}!`); // Якщо person — це число  
 }  
}  
greet("Аліса"); // Вивід: Привіт, Аліса!  
greet(25); // Вивід: Привіт, число 25!

Тип guard in використовується для перевірки наявності конкретної властивості в об'єкті, допомагаючи звузити тип.

interface Bird {  
 fly(): void;  
}  
interface Fish {  
 swim(): void;  
}  
function move(animal: Bird | Fish) {  
 if ("fly" in animal) {  
 animal.fly(); // Якщо це птах, він полетить  
 } else {  
 animal.swim(); // Якщо це риба, вона попливе  
 }  
}  
const bird: Bird = { fly: () => console.log("Лечу!") };  
const fish: Fish = { swim: () => console.log("Пливу!") };  
move(bird); // Вивід: Лечу!  
move(fish); // Вивід: Пливу!

Тип guard instanceOf:

Оператор instanceof використовується для перевірки, чи є об'єкт екземпляром конкретного класу або конструктора.
Це допомагає TypeScript звузити тип об'єкта.

class Animal {  
 sound() {  
 console.log("Звук тварини");  
 }  
}  
class Dog extends Animal {  
 sound() {  
 console.log("Гав");  
 }  
}  
class Cat extends Animal {  
 sound() {  
 console.log("Мяу");  
 }  
}  
function makeSound(animal: Animal) {  
 if (animal instanceof Dog) {  
 animal.sound(); // Якщо це собака, викликаємо sound() собаки  
 } else if (animal instanceof Cat) {  
 animal.sound(); // Якщо це кіт, викликаємо sound() кота  
 } else {  
 animal.sound(); // Якщо це інша тварина, викликаємо її звук  
 }  
}  
const myDog = new Dog();  
const myCat = new Cat();  
const genericAnimal = new Animal();  
makeSound(myDog); // Вивід: Гав  
makeSound(myCat); // Вивід: Мяу  
makeSound(genericAnimal); // Вивід: Звук тварини

Модифікатори доступу (Public, Private, Protected):

Public: Доступний будь-де.

Private: Доступний лише всередині класу.

Protected: Доступний всередині класу та підкласів.

class Employee {  
 // public, доступно звідусіль  
 public name: string;  
 // private, доступно тільки всередині цього класу  
 private salary: number;  
 // protected, доступно всередині цього класу і підкласів  
 protected position: string;  
 constructor(name: string, salary: number, position: string) {  
 this.name = name;  
 this.salary = salary;  
 this.position = position;  
 }  
 // public метод  
 public displayInfo(): void {  
 console.log(`Ім'я: ${this.name}, Посада: ${this.position}`);  
 }  
 // private метод  
 private calculateBonus(): number {  
 return this.salary * 0.1;  
 }  
 // protected метод  
 protected showSalary(): void {  
 console.log(`Зарплата: ${this.salary}`);  
 }  
}  
class Manager extends Employee {  
 constructor(name: string, salary: number, position: string) {  
 super(name, salary, position);  
 }  
 // Доступ до protected методу  
 public displaySalary(): void {  
 this.showSalary(); // Працює, тому що showSalary захищений  
 }  
}  
const emp = new Employee("Джон", 50000, "Розробник");  
console.log(emp.name); // public доступ  
emp.displayInfo(); // public метод працює  
// const emp2 = new Employee("Джон", 50000, "Розробник");  
// emp2.salary; // Помилка: 'salary' є private  
const mgr = new Manager("Аліса", 70000, "Менеджер");  
mgr.displayInfo(); // public метод працює  
mgr.displaySalary(); // protected метод працює в підкласі

Getter & Setter:

class Employee {  
 private _name: string;  
 constructor(name: string) {  
 this._name = name;  
 }  
 // Getter для _name  
 get name(): string {  
 return this._name;  
 }  
 // Setter для _name  
 set name(newName: string) {  
 this._name = newName;  
 }  
}  
const emp = new Employee("Джон");  
console.log(emp.name); // Вивід: Джон  
emp.name = "Аліса";  
console.log(emp.name); // Вивід: Аліса

Статичні властивості в OOP:

Static означає, що властивість або метод належать самому класу, а не його екземплярам, і спільно використовуються всіма об'єктами.

class Counter {  
 static count: number = 0; // Спільне для всіх екземплярів  
 increment() {  
 Counter.count++; // Збільшує спільне 'count'  
 }  
}  
const obj1 = new Counter();  
const obj2 = new Counter();  
obj1.increment(); // Counter.count стає 1  
obj2.increment(); // Counter.count стає 2  
console.log(Counter.count); // Вивід: 2

Інтерфейси для абстракції

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

interface Animal {  
 speak(): void;  
}  
class Cat implements Animal {  
 speak(): void {  
 console.log("Мяу");  
 }  
}  
const cat = new Cat();  
cat.speak(); // Вивід: Мяу

Перевантаження методів

Перевантаження методів дозволяє класу визначати кілька методів з однаковими іменами, але з різними типами параметрів.

class Printer {  
 print(value: string): void;  
 print(value: number): void;  
 print(value: any): void {  
 console.log(value);  
 }  
}

const printer = new Printer();  
printer.print("Привіт"); // Вивід: Привіт  
printer.print(123); // Вивід: 123

Композиція замість Наслідування

Композиція дозволяє створювати складну поведінку, комбінуючи прості, багаторазово використовувані компоненти, а не покладаючись на наслідування.

class Engine {  
 start() {  
 console.log("Двигун запущено");  
 }  
}  
class Car {  
 constructor(private engine: Engine) {}  

 drive() {  
 this.engine.start();  
 console.log("Автомобіль рухається");  
 }  
n Автомобіль рухається

Декоратори

Спеціальні функції, які модифікують класи або методи.

function Log(target: any, key: string) {  
 console.log(`Виклик методу ${key}`);  
}  
class Person {  
 @Log  
 sayHello() {  
 console.log("Привіт!");  
 }  
}  
const person = new Person();  
person.sayHello();   
// Вивід: Виклик методу sayHello \n Привіт!

Використання ключового слова super

Ключове слово super викликає методи з батьківського класу всередині дочірнього класу.

class Animal {  
 move() {  
 console.log("Тварина рухається");  
 }  
}  
class Dog extends Animal {  
 move() {  
 super.move();  
 console.log("Собака бігає");  
 }  
}  
const dog = new Dog();  
dog.move(); // Вивід: Тварина рухається \n Собака бігає

Міксини

Міксини дозволяють ділитися функціональністю між класами без наслідування.

type Constructor = new (...args: any[]) => T;  
function CanFly(Base: TBase) {  
 return class extends Base {  
 fly() {  
 console.log("Летить");  
 }  
 };  
}  
class Bird {}  
const FlyingBird = CanFly(Bird);  
const bird = new FlyingBird();  
bird.fly(); // Вивід: Летить

Розуміння OOP та SOLID на прикладі реального життя: Бізнес пекарні 🍰

Уявіть, що ви відкриваєте пекарню!

1.

Клас і Об'єкт

  • Клас: Уявіть "Рецепт торта". У ньому є інгредієнти та етапи, але це ще не торт.
  • Об'єкт: Коли ви слідуєте рецепту і печете торт, це вже Об'єкт!

2. Інкапсуляція

  • У вас є секретний рецепт глазурі. Ви зберігаєте його в заблокованому ящику, щоб ніхто не міг змінити його. Клієнти можуть насолоджуватися тортом, але не мають доступу до секретного рецепту.
  • Інкапсуляція захищає важливу інформацію, приховуючи її.

3. Наслідування

  • Ви готуєте базовий шоколадний торт. Потім ви створюєте "Шоколадний Лавовий Торт", додавши всередину рідкий шоколад. Ви не почали з нуля — ви просто розширили шоколадний торт!
  • Наслідування дозволяє повторно використовувати та розширювати існуючі ідеї.

4. Поліморфізм

  • Клієнт просить "Торт". Деяким подобається шоколадний, іншим — ванільний. Ви вмієте робити обидва, але всі вони просять просто торт!
  • Поліморфізм означає, що одна команда працює по-різному.

5. Абстракція

  • Клієнти бачать лише смачні торти на вітрині. Їм не потрібно знати, як працює піч або як піднімається тісто.
  • Абстракція приховує складні процеси і показує лише необхідні частини.

Розуміння принципів SOLID

1. Принцип єдиної відповідальності (SRP)

"Пекар" займається тільки випіканням тортів, а не їх декоруванням.

2. Принцип відкритості/закритості (OCP)

Ви можете додати новий смак торта, не змінюючи базовий рецепт.

3. Принцип підміни Ліскова (LSP)

Якщо "ШоколаднийТорт" є підтипом "Торт", клієнти повинні отримувати від нього таке ж задоволення, як і від будь-якого іншого торта.

4. Принцип сегрегації інтерфейсів (ISP)

Ваші пекарі потребують лише рецептів тортів, а касири — інструкцій для реєстрації.

5. Принцип інверсії залежностей (DIP)

Ваша пекарня залежить від постачальників інгредієнтів, але не від того, хто саме це постачає.

Висновок

Розуміння Об'єктно-орієнтованого програмування (OOP) та Принципів SOLID не обов'язково має бути лякаючим. Порівнюючи ці концепції з чимось простим і приємним, як робота пекарні, ми можемо побачити, як ці принципи застосовуються в реальних сценаріях. OOP допомагає нам ефективно структурувати наш код, роблячи його багаторазовим і легким для управління, а принципи SOLID надають нам напрямок для написання чистого, масштабованого та підтримуваного програмного забезпечення.

Як пекарня процвітає завдяки добре організованим рецептам і процесам, так і ваші програмні проекти можуть процвітати з сильними принципами проектування. Тож, будь то випікання тортів чи створення додатків, пам'ятайте, що правильна основа веде до солодкого успіху! 🍰💻

Щасливого кодування! 🚀

Перекладено з: Simplifying Object-Oriented Programming (OOP) Concepts with Real-Life Examples in TS

Leave a Reply

Your email address will not be published. Required fields are marked *