Що таке 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