DevLearn logo
Skill Up With Me
Interactive Learning
Signing in…

GRASP: распределение ответственностей между объектами

🔒 Sign in to use this

GRASP — General Responsibility Assignment Software Patterns — это набор из девяти принципов, введённых Крейгом Ларманом в книге «Применение UML и паттернов» (1997). Там, где SOLID описывает структуру отдельных взаимоотношений классов, GRASP отвечает на более фундаментальный вопрос, с которым сталкивается каждый разработчик при проектировании: какой объект должен отвечать за это поведение? GRASP предоставляет именованные эвристики для систематического ответа на этот вопрос, а не интуитивного. Паттерны — это не алгоритмы, а линзы, делающие процесс рассуждения явным и коммуникабельным.

ℹ️Паттерны GRASP — это эвристики проектирования, а не строгие правила. Несколько паттернов могут работать вместе над одним решением — например, Controller одновременно удовлетворяет паттернам Controller и Creator. Ценность GRASP не в применении каждого паттерна изолированно, а в наличии общего словаря для обсуждения назначения ответственностей.
Девять паттернов GRASP: обзор
Нажмите на каждую карточку для краткого обзора — детальные примеры следуют ниже
🧠
Информационный эксперт
🏭
Создатель
🎮
Контроллер
🔗
Слабое зацепление
🎯
Высокая связность
🔀
Полиморфизм
⚗️
Искусственная сущность
↩️
Косвенность
🛡️
Устойчивость к изменениям
Информационный эксперт: самый используемый паттерн

Информационный эксперт — наиболее часто применяемый паттерн GRASP и источник самого распространённого вопроса проектирования: «Куда поместить этот метод?» Ответ: в класс, обладающий данными, которые нужны методу. Если расчёт итога заказа требует знания строк заказа, класс Order (содержащий строки) должен вычислять свой собственный итог — а не внешний сервис, залезающий внутрь Order за строками.

Нажмите на подсвеченные строки, чтобы увидеть правильное применение паттерна и нарушение
java
1
// ── НАРУШЕНИЕ: поведение отделено от нужных ему данных ──────────
2
class OrderPricingService {
3
  Money calculateTotal(Order order) {
4
    return order.getItems().stream()                      // залезаем в Order
5
      .map(i -> i.getPrice().multiply(i.getQuantity()))   // используем данные Order
6
      .reduce(Money.ZERO, Money::add);
7
  }
8
}
9
10
// ── ИСПРАВЛЕНИЕ: ответственность — тому, кто владеет информацией ─
11
class Order {
12
  private final List<OrderLineItem> items;
13
14
  Money total() {
15
    return items.stream()
16
      .map(OrderLineItem::subtotal)  // делегирует эксперту каждой строки
17
      .reduce(Money.ZERO, Money::add);
18
  }
19
}
20
21
class OrderLineItem {
22
  private final Money price;
23
  private final int quantity;
24
  Money subtotal() { return price.multiply(quantity); }  // эксперт по собственным данным
25
}
Контроллер: разделение системных событий и доменной логики

Паттерн Контроллер отвечает на вопрос: кто должен обрабатывать системное событие (действие пользователя, входящее сообщение, плановый триггер)? Не доменные объекты — они должны не знать о механизмах доставки. Не UI — он должен не знать о бизнес-логике. Контроллер — это не-UI класс, который получает события и делегирует доменным объектам. Он координирует, но не вычисляет. На практике: REST-контроллер — это Контроллер по GRASP. Как и потребитель очереди, обработчик команды CLI или планировщик пакетного задания — все они контроллеры для разных механизмов доставки.

Нажмите на подсвеченные строки, чтобы увидеть паттерн Контроллер и его границы
java
1
// ── GRASP Контроллер: делегирует, не вычисляет ──────────────────
2
@RestController
3
class PlaceOrderController {
4
  private final PlaceOrderService service;
5
6
  @PostMapping("/orders")
7
  ResponseEntity<OrderResponse> place(@Valid @RequestBody PlaceOrderRequest req) {
8
    // Ответственности контроллера: получить, валидировать формат, преобразовать, делегировать
9
    PlaceOrderCommand cmd = req.toCommand();     // преобразуем входные данные
10
    Order order = service.placeOrder(cmd);       // делегируем ВСЮ логику
11
    return ResponseEntity.ok(OrderResponse.from(order)); // преобразуем результат
12
    // НИКАКОЙ бизнес-логики — ноль доменных правил в контроллере
13
  }
14
}
15
16
// ── Тот же домен, другой контроллер (Kafka-потребитель) ─────────
17
@KafkaListener(topics = "order-requests")
18
class PlaceOrderEventController {
19
  private final PlaceOrderService service;
20
  void handle(PlaceOrderEvent event) {
21
    service.placeOrder(event.toCommand());  // тот же сервис, другая точка входа
22
  }
23
}
Полиморфизм: замена условных операторов типами

Паттерн Полиморфизм адресует поведенческую вариативность, зависящую от типа. Когда вы видите switch или if/else по полю типа — это обычно сигнал применить паттерн: каждая ветка условного оператора должна стать подтипом, а поведение должно диспетчеризоваться системой типов, а не вызывающим кодом. Это прямая связь с OCP: switch растёт с каждым новым типом; полиморфизм означает добавление нового типа без изменения существующего кода.

Нажмите на подсвеченные строки, чтобы увидеть, как полиморфизм заменяет switch по типу
java
1
// ── НАРУШЕНИЕ: switch по типу, растущий с каждой новой скидкой ──
2
Money applyDiscount(Order order, DiscountType type) {
3
  return switch (type) {
4
    case PERCENTAGE -> order.total().multiply(0.9);
5
    case FIXED      -> order.total().subtract(Money.of(10));
6
    case BOGO       -> order.total().multiply(0.5);  // добавлено позже — метод изменён
7
  };
8
}
9
10
// ── ИСПРАВЛЕНИЕ: каждый тип инкапсулирует своё поведение ─────────
11
interface DiscountPolicy { Money apply(Money total); }
12
13
record PercentageDiscount(double rate) implements DiscountPolicy {
14
  public Money apply(Money total) { return total.multiply(1 - rate); }
15
}
16
record FixedDiscount(Money amount) implements DiscountPolicy {
17
  public Money apply(Money total) { return total.subtract(amount); }
18
}
19
record BogoDiscount() implements DiscountPolicy {
20
  public Money apply(Money total) { return total.multiply(0.5); }
21
}
22
23
// Вызывающий код — не меняется при добавлении новых типов скидок:
24
Money discounted = policy.apply(order.total());
Устойчивость к изменениям: корень всех паттернов

Ларман назвал Устойчивость к изменениям «наиболее фундаментальным принципом в разработке ПО» — и сложно с этим поспорить. Каждый паттерн проектирования, каждый архитектурный стиль, каждая абстракция существуют для того, чтобы защитить стабильный код от нестабильных изменений. Практическое применение: выявляйте точки нестабильности вашей системы (внешние API, часто меняющиеся бизнес-правила, технология базы данных, сторонние библиотеки) и оборачивайте каждую стабильным интерфейсом. Код, зависящий от интерфейса, защищён — он не изменится при изменении нестабильного за ним.

Нажмите на карточку, чтобы увидеть применение Устойчивости к изменениям в реальной системе
💳
Нестабильное: платёжный провайдер
🗄️
Нестабильное: технология базы данных
📜
Нестабильное: бизнес-правила
🌐
Нестабильное: внешний API
Искусственная сущность: когда доменных объектов недостаточно

Искусственная сущность разрешает противоречие между высокой связностью и необходимостью где-то разместить инфраструктурные задачи. Рассмотрим персистентность: сущность Order не должна знать, как сохранить себя в базу данных — это нарушило бы SRP и внесло инфраструктуру в домен. Но логика персистентности должна где-то жить. Repository — искусственная сущность: класс, изобретённый ради проектирования, не имеющий прямого аналога в предметной области. Он существует для того, чтобы дать поведению персистентности высокосвязный, слабосвязанный дом. Другие примеры: Mapper, Logger, EventPublisher, CacheManager — все они искусственные сущности.

Паттерны GRASP связаны с паттернами GoF — связь делает оба понятнее
Паттерн GRASPСоответствующий паттерн GoFЧто делает на практике
Информационный эксперт— (принцип, не паттерн)Метод живёт в классе с данными
СоздательFactory Method, Abstract FactoryСоздание объекта рядом с контекстом использования
КонтроллерFacade, CommandЕдиная точка входа для системных событий
Слабое зацеплениеFacade, Mediator, AdapterСнижает поверхность зависимостей между модулями
Высокая связность— (принцип, не паттерн)Связанное поведение в одном классе
ПолиморфизмStrategy, State, CommandДиспетчеризация по типу вместо условных операторов
Искусственная сущностьRepository, Mapper, LoggerИзобретённый класс для инфраструктурного поведения
КосвенностьAdapter, Proxy, Decorator, BridgeПромежуточный объект поглощает связанность
Устойчивость к изменениямВсе паттерны — это кореньСтабильный интерфейс вокруг точки нестабильности
Главный вывод: GRASP предоставляет девять именованных эвристик для наиболее распространённых решений по назначению ответственностей в объектно-ориентированном проектировании. Информационный эксперт держит поведение рядом с данными. Контроллер отделяет доставку от домена. Полиморфизм заменяет условные операторы типами. Искусственная сущность даёт инфраструктуре чистый дом. Устойчивость к изменениям — объединяющий принцип за каждой абстракцией и каждым паттерном.
ℹ️Что дальше: SOLID и GRASP регулируют назначение ответственностей внутри классов и между ними. Следующий урок делает шаг назад к трём сквозным эвристикам — KISS, DRY и YAGNI — которые управляют общим бюджетом сложности дизайна, и объясняет Принцип наименьшего удивления, связывающий их воедино.
🔒 Sign in to use this