Розділ 1. Еволюція болю: від GOTO до ООП

Надворі 2026-й. У нас є все: ШІ-асистенти, хмарні середовища розробки, фреймворки, які ледь не читають наші думки. Здавалося б, живи і радій, але чому тоді бізнес досі платить шалені гроші софтвер-архітекторам, а сеньйорів все ще ганяють по «древніх» патернах на співбесідах? І чому проєкти, які починаються як «цукерочка», через рік стають непідтримуваним пеклом, яке всі бояться чіпати?

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

А все тому, що за цими фреймворками, хмарами й розумними технологіями все ще стоїть одна й та сама проблема. Складність.

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

Тож «відмотаємо» до самих витоків, коли програмісти ще були справжніми ковбоями, а пам’ять комп’ютера вимірювалася в кілобайтах.

Ера хаосу: Spaghetti Code та GOTO (1960-ті)

Ось ми в 60-х. Пишемо на асемблері, Fortran або ранньому BASIC. У нас немає функцій в сучасному розумінні. Немає циклів while чи for. У нас є лише одна «ковбойська» суперзброя: оператор GOTO.

А з ним типовий код того часу виглядав приблизно так:

1 Рядок 10: Зроби щось.
2 Рядок 20: Якщо умова Х — стрибни на рядок 500.
3 Рядок 500: Зроби щось і стрибни назад на рядок 30.

Це породило явище під назвою Spaghetti Code (спагеті-код). Якщо намалювати потік виконання програми стрілочками на папері, ці лінії переходів виглядатимуть як велика тарілка макаронів, де важко знайти початок і кінець. Тому зрозуміти логіку, просто читаючи код зверху вниз, було неможливо — доводилось тримати в голові всю карту цих заплутаних зв’язків.

Spaghetti Code

Такий код неможливо було масштабувати. Мозок не справлявся з хаосом, і розробка великих систем перетворювалася на замкнене коло, де кожен виправлений баг створював десяток нових. 

У 1968 році стався злам підходу. Великий і жахливий1 Едсгер Дейкстра (Edsger W. Dijkstra) пише свого знаменитого листа в ACM2: «Go To Statement Considered Harmful». Фактично він заявив: «Хлопці, досить стрибати. Нам потрібна структура».

Це й дало початок структурному програмуванню, а розробники розклали хаотичні «макарони» в зручні «порційні контейнери».

Ера структурного програмування та його «спадщина» (1970-ті)

Вчені Коррадо Бем (Corrado Böhm) та Джузеппе Якопіні (Giuseppe Jacopini) математично довели, що будь-яку програму можна написати, використовуючи всього три конструкції:

  1. Послідовність (Sequence): роби А, потім Б.
  2. Розгалуження (Selection): if / else.
  3. Ітерацію (Iteration): while / for.

Усе. Більше ніяких стрибків GOTO. Об’єднання цих трьох конструкцій із процедурами та функціями, які дали можливість «запакувати» логіку в окремі блоки, стало справжнім проривом. Код став читабельним від початку й до кінця.

An icon of a info-circle1

Історична довідка: цікаво, що сам інструмент — підпрограми (subroutines), або окремі блоки коду для перевикористання, — з’явився задовго до формування структурної парадигми. Складність була в тому, щоб повернути виконання коду саме туди, звідки була викликана підпрограма, яка вже відпрацювала. Щоб розв’язати проблему, британський інженер Девід Вілер (David Wheeler) ще в 1949 році на комп’ютері EDSAC вигадав трюк, відомий як «стрибок Вілера» (Wheeler Jump): програма перед викликом сама записувала адресу повернення безпосередньо в останню інструкцію підпрограми. За це Вілер отримав перший у світі ступінь Ph.D. з комп’ютерних наук, захистивши дисертацію про автоматичні обчислення на EDSAC. Пізніше, у 1960 році, під час проєктування мови Algol 60, процедури отримали сучасний вигляд: локальні змінні на стеку та підтримку рекурсії. Проте розробники все ще продовжували писати код як заманеться, хаотично використовуючи GOTO разом із процедурами. І тільки в 1970-х структурне програмування закріпило їх як обов’язковий стандарт організації коду, остаточно витіснивши GOTO.

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

Одне з правил Дейкстри звучало так: «One Entry, One Exit» — один вхід, один вихід.

Для сучасного розробника вимога «одного входу» (One Entry) звучить як абсурд — бо як взагалі можна викликати функцію не з початку? Але в 60-х та 70-х роках на асемблері чи Fortran це було буденною справою. Програміст міг спокійно взяти GOTO і стрибнути прямо в середину тіла функції, оминаючи ініціалізацію локальних змінних. Код перетворювався на нечитабельний жах, а програма падала через кашу в регістрах пам’яті. Тому Дейкстра наполягав: увійти у функцію можна тільки через «парадні двері» — її початок.

З «одним виходом» (One Exit) історія інша. Для мов на кшталт Pascal чи C, де треба було руками чистити пам’ять перед виходом із функції, це мало критичне значення. Ти увійшов, виділив пам’ять, зробив роботу, очистив пам’ять, вийшов. В одній точці.

Але що ми бачимо сьогодні в Java, C# чи PHP? Розробники тягнуть це правило в сучасні мови буквально й, сліпо йому слідуючи, загортають ключовий функціонал в «Arrow Code», або «код-стрілу», з величезною кількістю вкладеностей.

 1 // Антипатерн Arrow Code (Спадщина "Одного виходу")
 2 public String processOrder(Order order) {
 3     String result = "Error";
 4     if (order != null) {
 5         if (order.isValid()) {
 6             if (order.hasPayment()) {
 7                 // 100 рядків логіки
 8                 result = "Success";
 9             }
10         }
11     }
12     return result; // Той самий "єдиний вихід"
13 }

Це антипатерн в сучасній розробці!

Такий  підхід ховає головну бізнес-логіку на самому «дні» вкладеностей — там, де її важко побачити й легко зламати.

Сьогодні ми сповідуємо принцип «Fail Fast». Якщо дані невалідні — робимо return одразу, на самому початку методу. В сучасному дизайні це називається Guard Clauses (загороджувальні конструкції) та зберігає код плоским, чистим і зрозумілим.

1 // Сучасний підхід: Guard Clauses (Загороджувальні конструкції)
2 public String processOrder(Order order) {
3     if (order == null) return "Error";
4     if (!order.isValid()) return "Error";
5     if (!order.hasPayment()) return "Error";
6     // 100 рядків логіки на першому рівні вкладеності
7     return "Success";
8 }

Не треба тягнути виконання до кінця функції заради святого правила сімдесятих. Те правило було для іншого контексту.

Хоча структурне програмування й розв’язало проблему з хаотичним потоком — тепер код рухався зверху вниз через if / else, цикли та функції — сама природа програм залишалася процедурною. Функції та дані продовжували існувати окремо. Тому чим більшими ставали проєкти, тим більше функцій ділили між собою спільні змінні та глобальний стан. Хаос нікуди не зник — він «переїхав» від керування потоком до керування даними.

Ера процедурного пекла: глобальні дані (1980-ті)

Перенесемося на десятиліття вперед, у 80-ті. Тогочасна архітектура нагадувала комунальну квартиру, в якій:

  • Змінні (дані) — це речі у спільному коридорі та кухні.
  • Функції — це мешканці.

Функція А прийшла, взяла змінну User, змінила їй ім’я. Функція Б прийшла і вирішила, що User більше не потрібен та видалила його. Функція В намагається нарахувати User зарплату… і програма падає з помилкою Null Reference. Хто винен? Хто змінив дані? Відстежити було практично неможливо. Коли системи виростали до сотень функцій та модулів і великих команд розробників, ситуація виходила з-під контролю. Будь-яка зміна в одному модулі відгукувалася несподіваними багами в іншому.

Global State Problem

В проєкті не існувало безпечних меж відповідальності. Потрібен був спосіб зачинити двері — дати кожному мешканцю окрему кімнату з замком та заборонити чіпати чужі речі.

Народження ООП: клітини й повідомлення

У цей час на горизонті індустрії з’являються скандинави Крістен Нюгорд (Kristen Nygaard) і Оле-Йохан Дал (Ole-Johan Dahl) зі своєю мовою Simula 67 (перша мова з об’єктами). А згодом — й Алан Кей (Alan Kay), «батько» мови Smalltalk.

Як біолог за освітою, Кей надихався живими клітинами. Кожна з них була для нього окремим мікросвітом. З мембраною, мітохондріями, ядром, ДНК та складними хімічними процесами всередині. Проте ззовні цей мікросвіт — просто капсула, що «спілкується» з іншими клітинами через обмін сигналами. Клітина А не має прямого доступу до внутрішньої «кухні» клітини Б. Вона просто надсилає «повідомлення».

З цієї ідеї і народилася інкапсуляція.

Відтепер дані — це святе. Ми ховаємо їх всередину об’єкта. Ніхто ззовні не має права модифікувати їх так, як йому заманеться. Хочеш щось зробити? Виклич метод. Надішли об’єкту «повідомлення».

An icon of a info-circle1

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

І чим більшою ставала система, тим більше таких взаємодій накопичувалося. Ніхто вже не міг безкарно залізти в приватні дані об’єкта Employee, але щоб просто надрукувати платіжку, клас SalaryCalculator мусив жорстко залежати від конкретного драйвера матричного принтера EpsonFX80 та файлової системи. Варто було оновити модель принтера — і розробнику доводилося переписувати логіку розрахунку податків, а C++ компилятор через пекло взаємних інклудів (Header Hell) йшов перезбирати весь проєкт на пів години.

Залишалося питання: якщо класи вже існують, то за якими правилами вони мають взаємодіяти між собою? Рішення надійшло звідти, звідки не чекали.

Від цегли до байтів: історія патернів (1977-1994)

У 1977 році Крістофер Александер (Christopher Alexander), професор архітектури будівництва, пише книгу «A Pattern Language». Він зрозумів, що проблеми в архітектурі повторюються. Тож не треба щоразу вигадувати велосипед, якщо можна описати повторювані рішення:

  • Context (контекст): де це відбувається?
  • Problem (проблема): яку типову ситуацію треба вирішити?
  • Solution (рішення): як це зробити перевіреним способом.

А коли десять років по тому (1987) двоє геніальних програмістів, Кент Бек (Kent Beck) та Ворд Каннінгем (Ward Cunningham), прочитали Александера — їх осяяло: «В коді ж те саме!». Архітектори будівель, по суті, розв’язують ту ж задачу: як зі стандартних блоків побудувати щось надійне й довговічне. Саме Бек та Каннінгем й описали декілька перших патернів програмування. Спочатку індустрія сприйняла це скептично. Проте в 1994 році стався переломний момент.

Четверо вчених — Еріх Ґамма (Erich Gamma), Річард Хелм (Richard Helm), Ральф Джонсон (Ralph Johnson) та Джон Вліссідес (John Vlissides) — випускають книгу Design Patterns та входять в історію як Банда Чотирьох (Gang of Four, GoF). У ній вони сформували 23 класичні проблеми проєктування й дали їм назви: Factory, Command, Observer…

З’явилася спільна мова. Тепер розробник з Києва міг сказати колезі з Каліфорнії: «Застосуй тут синглтон» — без малювання діаграм і зайвих пояснень на пів години.

Темний бік патернів

Здавалося б, щасливий кінець. Ми знайшли Святий Грааль. Проте ні, не цього разу.

Універсальна мова принесла з собою нові «хвороби»:

  • Patternitis (патерніт) та «дитячі травми»: спочатку це хвороба новачків. Прочитавши книгу GoF, розробник отримує «молоток», і все навколо починає здаватися йому цвяхом. Він пхає патерни туди, де достатньо одного if/else. Але найгірше починається потім: обпікшись на цьому й створивши непідтримуваного монстра, такий розробник вирішує, що «патерни — це взагалі маячня». Допрацювавшись до позиції тімліда, через цю свою «дитячу травму» він починає категорично забороняти підлеглим використовувати БУДЬ-ЯКІ патерни. Ця крайність дуже часто трапляється в нашій індустрії.
  • Синдром карго-культу: багато хто копіює структуру патерну, бо «так круто», але не розуміє проблеми, яку він вирішує.

Джуніори ліпили складні фабрики там, де вистачило б звичайного new. Код ставав надскладним.

Протверезіння: GRASP і SOLID

У 1997 році Крейг Ларман (Craig Larman) видає книгу «Applying UML and Patterns». Він бачив, до чого котиться хаос навколо GoF-патернів, і спробував повернути галузь до здорового глузду. Iдея Лармана зводилася до простого: «Хлопці, ви побігли поперед батька в пекло. Ви вчите складні архітектурні форми (GoF), але досі не знаєте базових речей!»

An icon of a info-circle1

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

Але саме ця книга й дала світові концепцію GRASP (General Responsibility Assignment Software Patterns), яка ставить у центр розробки не класи, а відповідальність в системі. У кого є дані, той і робить роботу (Information Expert). Хто об’єкт використовує, той його і створює (Creator). Це фундамент, без якого ваші синглтони й декоратори — лише купа сміття.

Пізніше, на початку 2000-х, з’являється Роберт Мартін (Robert C. Martin), відомий як «Uncle Bob». Він шукав свої відповіді на архітектурні болі індустрії, і знайшов їх у правилах гнучкого проєктування. Тому сьогодні його ім’я намертво зрослося з абревіатурою SOLID. Тут відразу треба розвіяти популярний міф: окремої книги «SOLID» ніколи не існувало.

Більше того, сам акронім «SOLID» вигадав не Роберт Мартін, а Майкл Фезерс (Michael Feathers), який помітив, що якщо скласти перші літери п’яти головних принципів Мартіна з його монографії «Agile Software Development, Principles, Patterns, and Practices», вийде красиве слово. Принципів у ній набагато більше, але ми фокусуємось на базовій п’ятірці.

По суті, SOLID — це ваша «техніка безпеки», яка гарантує, що зміна одного класу не зруйнує інший.

Велика картина (The Big Picture)

Тепер у вас має скластися цілісний пазл еволюції інженерної думки:

Knowledge Pyramid

Бачите логіку? Наша піраміда спускається від загальних архітектурних ідей все ближче й ближче «до землі» — тобто до вашого реального коду та серверів:

  • На самій вершині абстракції лежить ідеологія ООП і три її принципи: інкапсуляція, поліморфізм та наслідування.
  • Кроком нижче ми маємо GRASP — це приземлені правила розподілу відповідальності (у який клас покласти метод).
  • Ще ближче до реального коду знаходиться SOLID — «техніка безпеки», яка гарантує гнучкість зв’язків між вашими класами.
  • Далі йде GoF — шаблони для вирішення локальних, специфічних мікрозадач (як створити об’єкт в конкретних умовах). Важливо розуміти: GoF — це не готові рішення для комплексних проблем системи. Це типові «сферичні коні», дуже локалізовані цеглинки.
  • І вже після них, на самому фундаментальному рівні системної розробки, починається справжня, доросла макроархітектура (Enterprise Patterns, мікросервіси, HighLoad) — тобто те, як усі ці «дрібні» абстракції та патерни об’єднуються у величезну живу систему.

Більшість програмістів роблять одну й ту ж помилку: вони завчають патерни GoF, як вірші, не розуміючи їхньої ідеології. І більшість книжок тільки підсилюють такий ефект. Ми ж підемо правильним шляхом: спускатимемося від чистої ідеології крок за кроком, аж поки не опинимося в самій гущі реального Enterprise-коду. І коли ви зрозумієте цей базис, вам відкриється шлях до справжньої комплексної архітектури.

А як щодо функціонального програмування?

Можливо, ви думаєте: «Сергію, зараз хайп навколо функціонального програмування (ФП). Лямбди, стріми, незмінний стан (Immutability). Кажуть, що ООП застаріло. Чому ж ФП просто не знищило ООП й ці ваші патерни?».

Щоб одразу відбити атаки фанатів чистого ФП (яких у нашій індустрії вистачає), поясню технічні причини. Чисте ФП не підходить для реалій більшості сфер розробки (чи то Enterprise, e-commerce, мобільна розробка, фронтенд або геймдев) з трьох фундаментальних причин:

  1. Проблема стану (State). Користувацький інтерфейс (UI) — це суцільний мінливий стан. Кнопка натиснута або ні, вікно активне або ні, користувач свайпнув чи ні. У чистій ФП-парадигмі стану немає (Immutability). Намагатися малювати UI чи обробляти введення від користувача на чистому ФП — це моделювати динамічне середовище за допомогою статичних математичних формул. Це страшенно незручно й виглядає як мазохізм.
  2. Проблема послідовності (Sequence). Класична бізнес-транзакція (відкрили транзакцію -> перевірили баланс юзера -> списали гроші -> оновили статус -> закрили транзакцію) — це імперативний, послідовний алгоритм команд. ФП, за своєю природою, оперує категоріями обчислень, а не команд. Тому моделювати послідовні транзакції в чистому ФП — це надзвичайно складна й неприродна задача.
  3. Побічні ефекти та монади (Side Effects). Логування, запис у БД, виклики сторонніх API — для чистого ФП це все «побічні ефекти». Чиста функція має лише прийняти дані й повернути результат без зовнішнього впливу. Щоб якось вирішити це обмеження (і щоб програма могла хоч щось записати в базу), фанати ФП придумали обхідний шлях — монади (на кшталт IO Monad). Але писати реальну бізнес-логіку на монадах — це гарантовано зламати мозок 90% вашої команди розробників.

Тому сучасна розробка працює на гібридній архітектурі:

  1. Макрорівень (бізнес-процеси та UI) — це ООП. Як тільки нам потрібна послідовна логіка, транзакції, збереження стану або малювання інтерфейсу — ми використовуємо ООП і патерни (контролери, сервіси). Побудувати великий Enterprise суто на функціях — це отримати непідтримуване спагеті з аргументів.
  2. Мікрорівень (трансформація даних) — це ФП. Усередині методів, коли треба просто відфільтрувати колекцію чи знайти потрібний елемент, ми використовуємо map, filter, чисті функції. Тут ФП безумовно перемагає.

Більше того, ФП часто спрощує класичні патерни. Наприклад, патерн Strategy в ООП вимагав інтерфейсу й низки класів. З появою лямбд він перетворився на просту передачу функції як параметра. Суть залишилася (ізолювати алгоритм від клієнта) — синтаксис спростився.

Тож ми не будемо воювати за «правильну» парадигму. Ми будемо прагматиками. Ми розберемося, як керувати складністю так, щоб код, який ви напишете, через рік усе ще хотілося підтримувати.

Сучасність: як виглядають патерни зараз?

Раніше, у дев’яностих, патерни GoF жили безпосередньо у вашому коді. Ви щоразу писали масивні класи ітераторів, спостерігачів і багатоповерхових фабрик вручну.

Сьогодні ситуація докорінно змінилася: більшість фундаментальних патернів «переїхали під капот» сучасних фреймворків та мов програмування:

  • Iterator: вам більше не треба створювати клас ітератора власноруч. Ви пишете foreach в C# або for..of у JavaScript. Патерн вшитий безпосередньо в синтаксис мови.
  • Observer: в екосистемах React, Vue чи Angular це основа реалізації реактивності (Hooks, Signals). У бекенд-фреймворках (наприклад, Laravel або Spring) Observer «чудово почувається» в системах Events і Listeners. Де-факто, ви використовуєте цей патерн щодня, навіть не замислюючись про його GoF-коріння.
  • Singleton та Prototype: у Spring Framework ви просто ставите анотацію @Scope("singleton") над біном, і IoC-контейнер сам керує життєвим циклом єдиного об’єкта системи.
  • Decorator: у Python і TypeScript цей складний структурний патерн став звичайною синтаксичною конструкцією @decorator.
An icon of a info-circle1

Важливо: може здатися, що патерни більше не потрібні, оскільки розробники фреймворків уже все написали за вас. Але насправді вам треба знати патерни не для того, щоб щоразу збирати їх з нуля (хоча іноді для складної бізнес-логіки це необхідно). Ви мусите їх знати, щоб бачити архітектурну суть за синтаксичним цукром сучасних інструментів. Без розуміння логіки патерну ви ніколи повноцінно не збагнете, чому ваш фреймворк поводиться саме так.

Готові зануритись у цю глибинну суть? Тоді перегортайте сторінку — рухаємося до архітектурних драйверів.

🔑 Висновки розділу

Що варто запам’ятати з цієї історії «болю»:

  • Еволюція — це вирішення попередньої проблеми. Структурне програмування вбило Spaghetti Code, а ООП перемогло «комуналку» глобальних станів. Не знаючи цього болю, ви не зрозумієте логіку патернів.
  • Сліпе слідування правилам минулого — антипатерн. Старе правило «одна точка виходу з функції» зараз породжує нечитабельний Arrow Code. Замінюйте його на сучасний Fail Fast (Guard Clauses).
  • Патерн — це інструмент, а не самоціль. Якщо ви застосовуєте GoF-патерн (стратегію, фабрику) там, де достатньо звичайного if/else — ви «захворіли на патерніт» та займаєтесь оверінжинірингом.
  • Від абстракції до «землі». Архітектурне мислення будується згори донизу: спочатку розуміння ідеології ООП, потім правила розподілу відповідальності (GRASP), далі техніка безпеки зв’язків (SOLID), потім мікрорішення (GoF) і лише на найнижчому (найширшому) рівні — макроархітектура (Enterprise, мікросервіси та HighLoad).
  • Жодної війни парадигм. Ми живемо в епоху гібридної архітектури. ООП ідеально працює на макрорівні (структура модулів і класів), а функціональне програмування рятує нас від проблем стану (Mutable State) на мікрорівні всередині функцій.
  • Знаходьте архітектурну суть. Багато класичних патернів (Iterator, Singleton, Observer) зараз «вшиті» безпосередньо в синтаксис мов або заховані «під капотом» фреймворків (Hooks, анотації @Scope, DI-контейнери). Ми вивчаємо патерни не для того, щоб писати їх з нуля, а щоб розуміти справжню логіку роботи наших сучасних інструментів.

  1. Дейкстру називали «великим і жахливим» через його безкомпромісний характер та гострий язик. Лауреат премії Тюрінга, творець структурного програмування та автор легендарного алгоритму пошуку найкоротшого шляху, він нещадно критикував індустрію та популярні мови програмування. Його вислів про те, що «навчання мови BASIC систематично калічить розум студентів…», став культовим.↩︎

  2. ACM (Association for Computing Machinery) — Асоціація обчислювальної техніки, найстаріша та найбільша у світі міжнародна наукова й освітня організація в галузі комп’ютерних наук.↩︎