Ви вивчили SOLID, але архітектура все одно кульгає? Чому GRASP важливіший за GoF (і як їх поєднати)
Привіт. Мене звати Сергій Немчинський, я програміст з досвідом більше 20 років, а ще засновник та власник школи ІТ-професій FoxmindEd.
У 2025 році я написав цикл статей про принципи SOLID, яких має дотримуватись кожен поважаючий себе програміст. Отримав неабиякий фідбек, в тому числі і критику. Мовляв, джуни навчаться того SOLID-у, а на реальних проєктах все одно працювати не вміють. До біса той SOLID.
Не можу не погодитися з першою частиною звинувачень: так, ситуація знайома. Приходить на співбесіду junior або early middle, може без проблем перелічити GoF-патерни і навіть пояснити, чим Factory відрізняється від Abstract Factory. Але впевненість миттєво зникає, коли доходить до реальних задач.
Проблема тут не в SOLID, а у відсутності архітектурного мислення. Що це таке, де його беруть і до чого тут патерни GRASP — ось про це і буде ця стаття.
Чому знання принципів SOLID та GoF-патернів не рятує
Мабуть, це традиція, успадкована ще з радянських часів, а може й раніше: сакралізація теоретичного знання. Тобто як зазвичай побудоване навчання:
«Юний падаван, оце п’ять принципів SOLID, а це класичні патерни проєктування GoF, їх всього 23, вивчи їх і най буде з тобою сила»
На непоганих курсах майбутніх розробників ще навчають використовувати ці патерни на умовних прикладах.
Такий джун відкриває практичну задачу і звично думає:
«Який патерн тут можна використати?»
Тобто правило стає самоціллю. Це дуже тонкий, але критичний зсув мислення. Рішення починає підганятися під шаблон, а не під реальну задачу.
У результаті код збільшується у розмірі, починає обростати страховками. Замість одного великого класу зʼявляється тридцять маленьких. Кожен щось робить, але ніхто не може пояснити, який клас за що відповідає і навіщо він тут взагалі існує.
А як має думати розробник? Ну приблизно ось так:
- Яку відповідальність має взяти на себе цей клас?
- Яку конкретну проблему він вирішує в живому коді?
- Де саме створювати обʼєкти?
- Куди «покласти» нову поведінку, щоб завтра код не розсипався?
Якщо ви бачили мій YouTube-канал, вам знайома моя позиція: в програмуванні процес написання коду займає не більше 30% часу. А весь інший час — розмірковування та планування. Але це я такий розумний після 20 років досвіду, а що з джунами?
Проблема в тому, що джунів зазвичай не вчать приймати архітектурні рішення. Не показують, як думати про відповідальності, ролі та межі між обʼєктами. А без цього навіть найкращі принципи і патерни перетворюються на набір формальних правил.
Саме про цю прогалину й піде мова далі.
Що таке GRASP і чому про них майже не говорять
Коли ми пишемо код, то використовуємо принципи SOLID та патерни GoF. Але перед цим існує етап прийняття рішень. На цьому етапі теж потрібний інструмент, який допоможе розкласти відповідальності, зрозуміти ролі об’єктів та визначитись з логікою ще до вибору патерна.
І саме тут зʼявляються GRASP — не як альтернатива GoF, а як фундамент під ними. GRASP розшифровується як General Responsibility Assignment Software Patterns. Назва звучить складно, але суть у них дуже приземлена. Це не патерни в тому сенсі, в якому ми звикли говорити про Singleton чи Strategy. Тут немає готових класів, діаграм чи шаблонного коду.
GRASP — це принципи розподілу відповідальності. Вони відповідають на базові, але критично важливі питання:
- який клас має відповідати за цю логіку?
- хто створює обʼєкти?
- де точка входу в сценарій?
- де закінчується відповідальність одного обʼєкта і починається іншого?
Власне, GRASP — це про те, як думати перед тим, як писати код. Але вони вимагають думати, їх не можна просто загуглити і вставити шматок коду. До того ж у них майже немає UML-діаграм, їх складно показати на слайді або в туторіалі. Ось і виходить, що про GRASP знають значно менше, ніж про GoF: вони менш маркетингові.
У результаті GRASP часто пролітають повз навчальні курси і статті, хоча саме вони найбільше впливають на якість архітектури.
Чому відсутність GRASP — велика проблема
Зафіксуємо тут відправну точку моєї думки: більшість проблем у живих системах виникає не через мову програмування і не через фреймворк. Вони виникають через неправильне мислення про відповідальність. Хто що має робити, і що не менш важливо, хто чого робити не має.
Саме з цього народжується такий феномен, як «божественні» сервіси. Так називають клас, який робить занадто багато, вирішує в системі все, наче він бог. Зазвичай виростає органічно, через відсутність чіткого розуміння відповідальності.
Спочатку є нормальний маленький сервіс. Потім у проєкті з’являється нова задача, і хтось думає: «О, тут уже є сервіс, давай просто додамо ще один метод». Потім ще один.Через пів року цей клас знає весь бізнес, усі репозиторії, всі інтеграції й половину домену. Формально код працює. Архітектурно — це катастрофа.
По-перше, такий сервіс неможливо нормально тестувати. Щоб протестувати один сценарій, треба підняти пів системи або написати купу моків.
По-друге, будь-яка зміна бізнес-логіки ламає його з усіх боків. Клас має десятки причин для зміни, тому кожен коміт стає ризикованим.
По-третє, такий сервіс стає точкою знання про всю систему. Новий розробник відкриває цей файл і бачить 800 рядків логіки, яка не має чіткої структури.
З точки зору GRASP «божественний» сервіс порушує одразу кілька принципів:
— Creator: він створює обʼєкти, якими не володіє;
— Controller: він одночасно і точка входу, і виконавець;
— High Cohesion: у ньому намішано все підряд;
— Low Coupling: він залежить від усього і всі залежать від нього.
Це не єдина можлива проблема, ще можливі і контролери по п’ятсот рядків, і класи-фабрики, які створені просто тому, що «десь читали, що так правильно». Але я пишу не про них, а про те, як їх позбутися і не допустити знов.
GRASP на практиці: три принципи як два пальці
Розглянемо три принципи, на яких найчастіше спотикаються реальні проєкти.
Creator: хто насправді має створювати обʼєкти
Найпопулярніша помилка — створювати обʼєкти там, де це просто зручно в моменті. У контролері, у сервісі, або ж ховати все це за фабрикою, навіть якщо вона насправді нічого не вирішує. У результаті в умовному інтернет-магазині контролер починає створювати Order, потім OrderItem, потім ще й Invoice, паралельно рахуючи суми. Логіка розмазується, залежності ростуть, будь-яка зміна починає боліти.
GRASP Creator формулює дуже просту ідею. Обʼєкт має створювати той, хто вже має всі необхідні для цього дані або логічно володіє створюваним обʼєктом. Якщо Order знає, з чого складається замовлення, то саме він і має створювати OrderItem. Якщо рахунок — частина життєвого циклу замовлення, то створення Invoice природно лежить усередині Order, а не в контролері.
Як тільки це правило починає працювати, магії не відбувається. Просто зникає зайвий шум. Контролери худнуть, сервіси втрачають божественність, а фабрики залишаються тільки там, де вони дійсно потрібні.
Controller: точка входу, а не місце роботи
Друга класична проблема — неправильне розуміння ролі контролера. У багатьох проєктах контролер перетворюється на смітник. Він валідує, створює доменні обʼєкти, рахує, зберігає, викликає інтеграції і ще й формує відповідь. Формально це контролер, по факту — сервіс з HTTP-анотаціями.
GRASP Controller каже дуже неприємну для багатьох річ: контролер — це не місце бізнес-логіки. Це точка входу в систему. Він має прийняти запит, передати керування відповідному use case і повернути результат. Уся предметна логіка при цьому живе в окремих класах application-рівня.
Правильний контролер не дорівнює сервісу, він просто зв’язує зовнішній світ із внутрішнім. Коли все зроблено саме так, код стає більш передбачуваним та прозорим.
Pure Fabrication: коли штучні класи рятують дизайн
Третій принцип зазвичай викликає або страх, або фанатизм. Або розробник боїться створювати нові класи й пхає все в доменні сутності, або ж навпаки, робить один великий Service, який займається всім одразу.
GRASP Pure Fabrication дозволяє чесно сказати: так, цей клас не відображає домен напряму. І це нормально. Його завдання зменшити звʼязність і зробити систему читабельною. Репозиторії, інтеграційні сервіси, мапери, білінг, логування — усе це не домен, але без цього домен дуже швидко перетворюється на звалище технічних деталей.
Pure Fabrication це спосіб захистити предметну модель від того, що їй не належить.
Як із GRASP природно випливає SOLID
Тут важливо зробити паузу і зробити очевидний висновок. SOLID — це не відправна точка дизайну системи, а його наслідок. GRASP відповідає на питання «кому що робити», а SOLID — на питання «наскільки добре ми це зробили».
Коли Creator застосований правильно, створення обʼєктів не розмазане по всій системі, а логіка зʼявляється там, де їй природно бути, у результаті класи перестають займатися всім підряд і отримують одну зрозумілу причину для зміни. Це і є принцип Single Responsibility, але не як догма з книжки, а як природний ефект здорового дизайну.
Коли контролер залишається точкою входу, а не бізнес-двигуном, логіка вимушено виноситься в окремі сервіси і use case. Контролер починає залежати не від конкретних реалізацій, а від абстракцій. Так зʼявляється Dependency Inversion. Нетому, що так правильно, а тому, що інакше система просто не масштабується.
Pure Fabrication, своєю чергою, дозволяє додавати нову поведінку, не ламаючи існуючий код і не тягнучи технічні залежності в домен. Це автоматично дає Open/Closed і низьку звʼязність, без спеціальних зусиль і магічних інтерфейсів.
Отже, я наголошую: якщо GRASP застосований усвідомлено, SOLID не потрібно впроваджувати силоміць. Він зʼявляється сам. SOLID без GRASP зазвичай виглядає формально і механічно: давайте додамо ще один інтерфейс, бо так треба. SOLID разом із GRASP виглядає живим. У ньому кожна абстракція має причину, а дизайн легко пояснюється здоровим глуздом, а не цитатами з книжок.
А де ж тут GoF-патерни?
Та осьдечки, поруч. GRASP, SOLID і GoF — не конкуренти, а рівні одного процесу. В ідеалі правильна послідовність мислення розробника має виглядати наступним чином.
- GRASP — розподіляємо відповідальність. Який клас що робить? Де логіка має жити? Шо по сценаріям?
- SOLID — перевіряємо якість дизайну коду. Чи не перевантажені класи? Чи легко міняти реалізації?
- GoF — підбираємо інструмент реалізації. Який шаблон найкраще реалізує це рішення?
На практиці це може виглядати ось так. Є задача — розробити модуль для онлайн-магазину з цінами, що динамічно змінюються залежно від зовнішніх чинників.
Використовуємо GRASP, думаємо, як це вписати в існуючий софт, щоб не завадити його нормальній роботі. Розуміємо: потрібен окремий клас для вибору алгоритму розрахунку ціни.
Застосовуємо SOLID, думаємо, як зробити це красиво і чисто. Робимо висновок: клас має залежати від інтерфейсу, а не від конкретної реалізації.
Нарешті добрались до GoF. Тут ідеально лягає Factory, тому що потрібно створювати реалізації залежно від умов.
Наче і нескладно. Але тонке місце в тому, що цей спосіб мислення майже неможливо отримати на теорії. Ну хіба що ви сядете в позу лотос і будете медитувати, прокручуючи ці сценарії в голові й уявляючи рядки коду. Але мені такі розробники не зустрічались.
Тому треба брати поганий код (можна власний, ми не засуджуємо), покращувати його крок за кроком, та пояснювати (можна собі), чому так буде краще. Таким чином через біль, погані рішення та рефакторинг напрацьовується архітектурне мислення.
А без чіткого розподілу відповідальності (GRASP) та якісних залежностей (SOLID) GoF-патерни перетворюються на архітектурний карґо-культ, при якому зміст підмінюється формою, а код залишається лайняним.
Висновок
Якщо ви дочитали аж досюди, скоріш за все, вам ця проблема вже знайома. Ви вже переросли туторіали для новачків і готові працювати з дизайном коду системно. Наступний етап — вам треба вчитися бачити відповідальності та вміти їх перерозподіляти, тобто вчитися мислення архітектора.
Ось вам коротка формула для запамʼятовування:
GRASP → мислення
SOLID → критерії якості
GoF → інструментарій
І моя порада: краще за все практичні навички застосування GRASP напрацьовувати на рефакторінгу. У світі існує достатньо поганого коду, який можна (і треба) покращити.
Як завжди, дякую за увагу і чекаю на коментарі.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

52 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів