Що таке ООП у програмуванні простими словами: 4 принципи з прикладами

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

Ідеї, з яких виросло ООП, у 1960-х утілили в мовах Simula I та Simula 67 норвезькі науковці Уле-Юган Дал і Крістен Нюґор. За ці мови обидва отримали премію Тюрінга 2001 року. Сам термін американський інформатик Алан Кей, за власними словами, уперше вжив приблизно 1967 року. Об’єкти він уявляв подібними до живих клітин або комп’ютерів у мережі, які обмінюються лише повідомленнями, — так Кей пояснював у листі 2003 року. У 1972-му його група в Xerox PARC створила мову Smalltalk, а 2003-го Кей теж став лауреатом премії Тюрінга.

ООП — одна з парадигм програмування поряд із процедурною та функціональною, і сучасні мови часто їх поєднують. Наприклад, мова C#, за описом Microsoft, побудована на принципах ООП, але запозичила чимало можливостей з інших парадигм, зокрема з функціонального програмування. Не плутайте з освітнім скороченням ООП — «особливі освітні потреби»: тут ідеться про програмування.

Клас і об’єкт простими словами

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

Дані об’єкта називають атрибутами, або полями, а дії — методами. Посібник Oracle dev.java так і визначає клас: креслення, за яким створюють окремі об’єкти. А в Python, за документацією, об’єктами є всі дані програми, навіть числа й рядки. Ось кавоварка на Python:

class CoffeeMachine:
    def __init__(self, water_ml):
        self.water_ml = water_ml

    def brew(self, cup_ml=200):
        if cup_ml > self.water_ml:
            return "Долийте води"
        self.water_ml -= cup_ml
        return "Кава готова"

home = CoffeeMachine(1000)
office = CoffeeMachine(150)
print(home.brew())    # Кава готова
print(office.brew())  # Долийте води

Python автоматично викликає метод __init__, коли створює об’єкт, і цей метод задає початковий стан. Параметр self — посилання на сам об’єкт; назва self, як пояснює офіційний підручник, — лише домовленість. Після home.brew() у домашній кавоварці лишилося 800 мл, а в офісній досі 150: кожен об’єкт зберігає власні дані.

Чотири принципи ООП з прикладами

Кожен принцип спершу пояснимо на кавоварці, а потім покажемо в коді на Python або Java.

Інкапсуляція: стан змінюють лише методи

Кавоварка не дає напряму виставити температуру нагрівача чи налити в бак мінус пів літра: ви натискаєте кнопки, а вона сама перевіряє, чи можлива дія. Це і є інкапсуляція. Дані й методи для роботи з ними зібрано в одному об’єкті, а внутрішній стан закрито від прямого втручання: змінити його можна тільки через методи, які стежать за правилами.

public class CoffeeMachine {
    private int waterMl;

    public void addWater(int ml) {
        if (ml <= 0) {
            throw new IllegalArgumentException("Об’єм має бути більшим за нуль");
        }
        waterMl += ml;
    }

    public int getWaterMl() {
        return waterMl;
    }
}

Поле waterMl позначене словом private, тож код поза класом не зможе написати machine.waterMl = -500: компілятор видасть помилку. Воду додають лише через addWater, а цей метод відкидає неможливі значення. Посібник dev.java радить обирати найсуворіший доречний рівень доступу і робити поля private, якщо немає вагомих причин робити інакше.

У Python модифікаторів доступу на кшталт private немає. Ім’я з підкресленням на початку (_water_ml) за домовленістю означає «внутрішнє, ззовні не чіпати». Ім’я з двома підкресленнями (__water_ml) Python перейменовує на _CoffeeMachine__water_ml, щоб уникнути конфлікту з однойменними атрибутами підкласів. Обидва правила описано в підручнику Python.

Успадкування: нове на основі готового

Кавоварка з капучинатором уміє все, що й звичайна, і ще збиває молоко. Її не треба креслити з нуля: досить узяти базову модель і додати деталь. Так працює успадкування: клас-нащадок (підклас) отримує поля й методи батьківського класу, а також додає свої або змінює успадковані. Слово «наслідування», яке теж часто вживають, — калька з російського «наследование»: українське «наслідувати» зазвичай означає «повторювати за кимось».

class CappuccinoMachine(CoffeeMachine):
    def __init__(self, water_ml, milk_ml):
        super().__init__(water_ml)
        self.milk_ml = milk_ml

    def froth_milk(self):
        if self.milk_ml < 100:
            return "Додайте молока"
        self.milk_ml -= 100
        return "Пінка готова"

m = CappuccinoMachine(1000, 300)
print(m.brew())        # Кава готова: метод успадковано
print(m.froth_milk())  # Пінка готова: новий метод

У Python батьківський клас пишуть у дужках після назви нового, а super() дає змогу викликати його методи. У Java для цього є слово extends і правило: у кожного класу, крім Object, рівно один прямий батьківський клас (dev.java). Python натомість дозволяє й множинне успадкування.

Успадкування доречне, коли нащадок справді є різновидом батька: кавоварка з капучинатором — це кавоварка. Якщо ж одне лише містить інше (у кавоварки є бак), бак роблять окремим об’єктом і зберігають у полі кавоварки. Це називають композицією, і автори класичної книжки Design Patterns радили надавати їй перевагу над успадкуванням класів.

Поліморфізм: одна команда, різна поведінка

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

interface CoffeeMaker {
    String brew();
}

class DripMachine implements CoffeeMaker {
    @Override
    public String brew() { return "Фільтр-кава готова"; }
}

class CapsuleMachine implements CoffeeMaker {
    @Override
    public String brew() { return "Еспресо з капсули готове"; }
}

// усередині методу main:
CoffeeMaker[] machines = { new DripMachine(), new CapsuleMachine() };
for (CoffeeMaker m : machines) {
    System.out.println(m.brew());
}

Змінна m має тип CoffeeMaker, але Java викликає метод того об’єкта, на який вона посилається, а не метод, оголошений у типі змінної. Документація називає це віртуальним викликом методу. Щоб додати ріжкову кавоварку, досить написати ще один клас, а цикл лишиться без змін.

Класи тут перевизначають (override) метод brew, тобто дають йому власну реалізацію. Не плутайте це з перевантаженням (overload): так називають кілька методів з однаковою назвою в одному класі, які компілятор Java розрізняє за кількістю й типами аргументів.

У Python спільний інтерфейс можна навіть не оголошувати: досить, щоб у кожного об’єкта був метод brew(). Такий стиль називають качиною типізацією: якщо щось схоже на качку і крякає як качка, то це качка.

Абстракція: лише те, що потрібно знати

Щоб зварити каву, не треба знатися на помпах і термоблоках: досить налити воду, засипати каву й натиснути кнопку. Абстракція в ООП — це виділення суттєвих для задачі властивостей і дій об’єкта без зайвих деталей. Інтерфейс CoffeeMaker з попереднього прикладу і є абстракцією: він гарантує, що будь-яка кавоварка вміє brew(), але не каже, як саме.

У Python абстракцію задають абстрактним базовим класом з модуля abc. Створити об’єкт абстрактного класу не можна, а його нащадка — лише тоді, коли в ньому перевизначено всі абстрактні методи:

from abc import ABC, abstractmethod

class CoffeeMaker(ABC):
    @abstractmethod
    def brew(self):
        ...

class CapsuleMachine(CoffeeMaker):
    def brew(self):
        return "Еспресо з капсули готове"

print(CapsuleMachine().brew())  # Еспресо з капсули готове
CoffeeMaker()                   # помилка TypeError

У Java є абстрактні класи та інтерфейси. Абстрактний клас може мати поля зі станом і готові методи, але розширити можна лише один клас, тоді як інтерфейсів клас реалізує скільки завгодно. Слово «інтерфейс» тут означає набір методів, а не екран програми. Як між собою домовляються цілі програми, пояснює матеріал «Що таке API простими словами».

Що таке SOLID

Чотири принципи пояснюють, як улаштоване ООП, а SOLID — як писати класи, які легко змінювати. Це п’ять правил, які популяризував Роберт Мартін, відомий як Uncle Bob; назву склали з перших літер їхніх англійських назв. Короткі формулювання він навів у статті 2020 року:

  • S — Single Responsibility, принцип єдиної відповідальності: разом тримають те, що змінюється з однієї причини, а те, що змінюється з різних, розділяють. Кавоварка варить каву, а облік витрат на зерно — справа іншого класу.
  • O — Open/Closed, принцип відкритості/закритості: модуль відкритий для розширення, але закритий для змін. Нову модель додають новим класом, не переписуючи старий код.
  • L — Liskov Substitution, принцип підстановки Лісков (на честь науковиці Барбари Лісков): будь-яку реалізацію інтерфейсу чи підклас можна підставити замість базового типу, і програма працюватиме правильно.
  • I — Interface Segregation, принцип розділення інтерфейсів: інтерфейси мають бути невеликими, щоб класи не залежали від того, що їм не потрібно.
  • D — Dependency Inversion, принцип інверсії залежностей: код верхнього рівня залежить від абстракцій на кшталт CoffeeMaker, а не від низькорівневих деталей.

Питання про ООП на співбесіді

ООП — класика технічних співбесід для початківців; що означає рівень Junior і чим він відрізняється від Middle та Senior, пояснює матеріал про грейди в ІТ. Перевірте, чи можете відповісти своїми словами й з власним прикладом:

  • Назвіть чотири принципи ООП і наведіть приклад кожного.
  • Чим клас відрізняється від об’єкта?
  • Чим абстрактний клас відрізняється від інтерфейсу і коли що обрати?
  • Чим перевизначення методу відрізняється від перевантаження?
  • Чому в Java немає множинного успадкування класів? Підказка: так мова уникає проблем множинного успадкування стану, а множинне успадкування типу дають інтерфейси (dev.java).
  • Коли краще обрати композицію, а коли успадкування?
  • Що означає кожна літера в SOLID?

Новини навчання в ІТ збирає рубрика «Освіта», а оновлення мов та інструментів — рубрика «Розробка».

Часті питання

Що таке ООП простими словами?

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

Чим інкапсуляція відрізняється від абстракції?

Абстракція визначає, що об’єкт показує назовні, тобто які методи потрібні тим, хто ним користується. Інкапсуляція відповідає за те, щоб сховати й захистити решту: поля й деталі реалізації.

Які мови програмування підтримують ООП?

Класи, успадкування й поліморфізм є, зокрема, у Java, C#, C++, Python, PHP, Kotlin і Ruby, а в JavaScript класи працюють поверх прототипів. Go дозволяє об’єктно-орієнтований стиль, але, як пояснює його FAQ, ієрархії типів у ньому немає.

Чи обов’язково знати ООП, щоб писати на Python?

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