Skip to main content

Що таке патерни grasp

GRASP (General Responsibility Assignment Software Patterns) - набір дев'яти принципів для визначення того, який клас повинен відповідати за яку частину логіки в об'єктно-орієнтованому дизайні.

Теорія

TL;DR

  • Аналогія: GRASP схожий на розподіл ролей на кухні. Шеф-кухар (Information Expert) готує страви, бо знає рецепти, а не офіціант.
  • Центральна ідея: призначай задачу класу, який має потрібні дані.
  • Головна різниця від SOLID: GRASP відповідає на питання хто що робить, SOLID відповідає як влаштовані класи.
  • Три принципи дають 80% цінності на практиці: Information Expert, Low Coupling, High Cohesion.
  • Не підходить для процедурного або функціонального коду без ієрархії класів.

Швидкий приклад

java
// Погано: Controller знає забагато про бізнес-логіку Order class OrderController { void process(Order order) { if (order.getTotal() > 100) order.applyDiscount(0.1); // Не той власник } } // Добре: Information Expert - Order відповідає за свою знижку class Order { private double total; void applyDiscountIfEligible() { if (total > 100) total *= 0.9; // Order має дані, тому він відповідає за правило } }

Контролер лише координує. Правило знижки живе там, де живуть дані.

Що таке GRASP насправді

GRASP не є фреймворком або бібліотекою. Жоден компілятор не перевіряє його. Це словник проектування: набір іменованих принципів, які застосовують на дошці, в UML-діаграмах або при ескізуванні структури класів до написання коду. Користь з'являється пізніше: менше змін по ланцюжку, коли вимоги оновлюються.

Craig Larman описав GRASP у книзі "Applying UML and Patterns", щоб зробити розподіл відповідальностей явним. До цього розробники приймали такі рішення інтуїтивно, без спільної мови для обґрунтування.

GRASP vs SOLID vs GoF

Три речі, які найчастіше плутають на співбесідах.

GRASP відповідає: який клас повинен відповідати за цю задачу? SOLID відповідає: як повинні бути структуровані окремі класи? GoF відповідає: яким патерном вирішити цю типову структурну проблему?

GRASP допомагає обрати або придумати патерн. GoF передбачає, що ти вже знаєш, коли його застосовувати.

GRASPSOLIDGoF
ФокусРозподіл відповідальностейПравила структури класівГотові рішення
ЕтапПочаткове моделюванняПостійне проектуванняВибір патерну
Результат"Цей клас відповідає за те"Краща структура класівКонкретний патерн
АвторCraig LarmanКілька (Martin та ін.)Gang of Four

Дев'ять принципів

Information Expert. Призначай відповідальність класу, який має дані для її виконання. Order перевіряє товари, бо він їх містить. Не Controller, не утилітний клас.

Creator. Клас B повинен створювати екземпляри класу A, якщо B містить, агрегує, записує або тісно використовує A. PaymentGateway створює об'єкти Charge, бо управляє платіжним контекстом.

Controller. Зовнішні події (кліки UI, API-запити) проходять через спеціальний клас контролера. Контролер делегує доменним об'єктам і не виконує доменну логіку самостійно.

Low Coupling. Мінімізуй залежності між класами. Чим менше класів ламається при зміні одного, тим краще. Передавай лише те, що метод реально використовує.

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

Polymorphism. Варіативну поведінку залежно від типу реалізовуй через поліморфізм, а не if/else. Коли поведінка змінюється за типом у runtime, інтерфейси та підкласи впораються краще.

Pure Fabrication. Іноді жоден доменний клас не підходить для певної відповідальності. Тоді створюється штучний клас, який не представляє доменну концепцію, але існує для підтримки Low Coupling і High Cohesion. Класичний приклад - AuditLogger.

Indirection. Додай посередника між двома класами, які не повинні залежати один від одного напряму. Message brokers, фасади, адаптери - все це Indirection на практиці.

Protected Variations. Обгорни все, що може змінюватись, за стабільним інтерфейсом. Код, який викликає інтерфейс, залишається незмінним навіть коли реалізація за ним змінюється.

Коли застосовувати GRASP

  • Новий доменний модуль з нуля: застосуй усі дев'ять, щоб розподілити ролі класів до написання коду.
  • Рефакторинг "бога класу" (god class): High Cohesion і Low Coupling покажуть, де ділити.
  • Командне проектування: назвати принцип означає аргументувати рішення конкретно, а не суб'єктивно.
  • Межі мікросервісів: Information Expert підказує, який сервіс відповідає за які дані.

Не підходить для процедурних скриптів або функціональних пайплайнів без ієрархії класів.

Типові помилки

1. Controller як звалище бізнес-логіки.

GRASP-контролер є тонким координатором. Коли він починає накопичувати доменні правила, це порушення Information Expert. Більшість кодових баз починають саме так, бо контролер є очевидною точкою входу. Рішення: делегуй якомога раніше.

java
// Погано: контролер виконує доменну логіку class OrderController { void process(Order order) { // 80 рядків логіки знижок, податків і доставки } } // Добре: контролер делегує class OrderController { void process(Order order) { orderService.applyEligibleDiscounts(order); shippingService.calculateCost(order); } }

2. Передавати цілий об'єкт заради одного поля.

Якщо методу потрібне лише одне поле, передача всього об'єкта створює зайву залежність. Додаєш поле до Order - і всі методи, що приймають його, стають потенційними жертвами змін.

java
// Погано: приймає весь Order, використовує тільки total void printReceipt(Order order) { System.out.println(order.getTotal()); } // Краще: передай те, що потрібно void printReceipt(double total) { System.out.println(total); }

3. Поліморфізм для простих статичних випадків.

Поліморфізм виправданий, коли поведінка змінюється у runtime. Для двох фіксованих випадків він зайвий. Створювати підкласи Discount10Percent і Discount20Percent замість if/else - передчасна абстракція. Два-три випадки: if/else. Варіація у runtime: поліморфізм.

4. Розкидані виклики new замість Creator.

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

Де зустрічається

  • Spring Framework: @Service-класи є GRASP-контролерами; @Entity-класи є Information Experts.
  • React/Redux: редьюсери дотримуються High Cohesion (тільки чиста логіка стану); action creators дотримуються Creator.
  • Java EE: EntityManager є Creator для JPA-сутностей.
  • Stripe API: PaymentGateway створює об'єкти Charge, а не клієнтський код.
  • Мікросервіси: Information Expert підказує, який сервіс відповідає за який набір даних.

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

Q: Поясни Information Expert на поганому і хорошому прикладі.
A: Погано: UI-компонент розраховує суму замовлення, бо має її відобразити. Він тепер залежить від кожного поля в розрахунку. Добре: Order.calculateTotal() відповідає за логіку, бо має дані. UI просто викликає метод.

Q: Як Low Coupling відрізняється від Dependency Inversion?
A: Low Coupling є метою: мінімізувати залежності загалом. Dependency Inversion є технікою: залежати від абстракцій, а не від конкретних класів. DIP є одним зі способів досягти Low Coupling, але не єдиним.

Q: Коли використовувати Pure Fabrication замість методу в доменному класі?
A: Коли відповідальність не належить жодній доменній концепції. Логування, аудит, форматування для експорту - це не доменна поведінка. Додавати їх у доменний клас означає ламати його згуртованість.

Q: Спроектуй аукціонну систему з трьома принципами GRASP (рівень senior).
A: Bid як Information Expert (знає свою валідність за мінімальною ціною і лімітами ставок). Auctioneer як Controller (отримує події ставок, делегує BidValidator). Low Coupling через інтерфейс BidValidator, щоб Auctioneer не залежав від конкретної логіки валідації.

Q: High Cohesion vs Single Responsibility Principle: це одне й те саме?
A: Пов'язані, але різні. High Cohesion групує споріднені поведінки в одному класі. SRP каже, що клас повинен мати лише одну причину для змін. Клас може бути згуртованим, але порушувати SRP: відповідати і за авторизацію, і за відображення профілю. Обидва пов'язані з "користувачем", але змінюються з різних причин.

Q: Як принципи GRASP відображаються на функціональне програмування?
A: Information Expert відображається на чисті функції, що отримують потрібні дані як аргументи. Low Coupling відображається на композицію функцій з мінімальним спільним станом. Назви різні, ідеї перетинаються.

Приклади

Information Expert: Order перевіряє себе сам

java
// Погано: Client дублює внутрішню логіку Order class Client { void validateOrder(Order order) { if (order.getItems().isEmpty()) throw new IllegalStateException("Порожнє замовлення"); for (Item item : order.getItems()) { if (!item.isInStock()) throw new IllegalStateException("Немає в наявності"); } } } // Добре: Order має список товарів, тому перевіряє себе сам class Order { private List<Item> items; boolean isValid() { return !items.isEmpty() && items.stream().allMatch(Item::isInStock); } }

Client змінюється рідше. Логіка перевірки наявності живе в одному місці. Коли правило валідації змінюється, торкаєшся одного класу.

Creator: PaymentGateway створює об'єкти Charge

javascript
// Натхнено моделлю ресурсів Stripe class PaymentGateway { createCharge(amount, customer) { // Gateway управляє Charge-об'єктами - він природний Creator return new Charge(amount, customer.id); } } class Charge { constructor(amount, customerId) { this.amount = amount; this.customerId = customerId; } } const gateway = new PaymentGateway(); const charge = gateway.createCharge(100, { id: 'cus_123' }); // Виводить: Charge { amount: 100, customerId: 'cus_123' }

Викликаючий код ніколи не пише new Charge(...). Все створення проходить через об'єкт, який контекстуально управляє зарядами. Замінити реалізацію пізніше стає простіше.

Pure Fabrication: AuditLogger тримає компоненти чистими

jsx
// Погано: side effect логування в UI-компоненті function UserProfile({ user }) { console.log('Audit:', user.id); // UI-компонент не повинен про це знати return <div>{user.name}</div>; } // Добре: Pure Fabrication - AuditLogger не є доменною концепцією, // він існує, щоб кожен клас залишався сфокусованим class AuditLogger { static logUserView(userId) { console.log('Audit:', userId); } } function UserProfile({ user }) { AuditLogger.logUserView(user.id); return <div>{user.name}</div>; } // Виводить: 'Audit: 123'

AuditLogger не представляє нічого в бізнес-домені. Він існує, бо відповідальність за логування мала десь жити - не в UI-компоненті і не в доменному класі.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?