GRASP: распределение ответственностей между объектами
GRASP — General Responsibility Assignment Software Patterns — это набор из девяти принципов, введённых Крейгом Ларманом в книге «Применение UML и паттернов» (1997). Там, где SOLID описывает структуру отдельных взаимоотношений классов, GRASP отвечает на более фундаментальный вопрос, с которым сталкивается каждый разработчик при проектировании: какой объект должен отвечать за это поведение? GRASP предоставляет именованные эвристики для систематического ответа на этот вопрос, а не интуитивного. Паттерны — это не алгоритмы, а линзы, делающие процесс рассуждения явным и коммуникабельным.
Информационный эксперт — наиболее часто применяемый паттерн GRASP и источник самого распространённого вопроса проектирования: «Куда поместить этот метод?» Ответ: в класс, обладающий данными, которые нужны методу. Если расчёт итога заказа требует знания строк заказа, класс Order (содержащий строки) должен вычислять свой собственный итог — а не внешний сервис, залезающий внутрь Order за строками.
// ── НАРУШЕНИЕ: поведение отделено от нужных ему данных ──────────
class OrderPricingService { Money calculateTotal(Order order) {return order.getItems().stream() // залезаем в Order
.map(i -> i.getPrice().multiply(i.getQuantity())) // используем данные Order
.reduce(Money.ZERO, Money::add);
}
}
// ── ИСПРАВЛЕНИЕ: ответственность — тому, кто владеет информацией ─
class Order {private final List<OrderLineItem> items;
Money total() {return items.stream()
.map(OrderLineItem::subtotal) // делегирует эксперту каждой строки
.reduce(Money.ZERO, Money::add);
}
}
class OrderLineItem {private final Money price;
private final int quantity;
Money subtotal() { return price.multiply(quantity); } // эксперт по собственным данным}
Паттерн Контроллер отвечает на вопрос: кто должен обрабатывать системное событие (действие пользователя, входящее сообщение, плановый триггер)? Не доменные объекты — они должны не знать о механизмах доставки. Не UI — он должен не знать о бизнес-логике. Контроллер — это не-UI класс, который получает события и делегирует доменным объектам. Он координирует, но не вычисляет. На практике: REST-контроллер — это Контроллер по GRASP. Как и потребитель очереди, обработчик команды CLI или планировщик пакетного задания — все они контроллеры для разных механизмов доставки.
// ── GRASP Контроллер: делегирует, не вычисляет ──────────────────
@RestController
class PlaceOrderController {private final PlaceOrderService service;
@PostMapping("/orders") ResponseEntity<OrderResponse> place(@Valid @RequestBody PlaceOrderRequest req) {// Ответственности контроллера: получить, валидировать формат, преобразовать, делегировать
PlaceOrderCommand cmd = req.toCommand(); // преобразуем входные данные
Order order = service.placeOrder(cmd); // делегируем ВСЮ логику
return ResponseEntity.ok(OrderResponse.from(order)); // преобразуем результат
// НИКАКОЙ бизнес-логики — ноль доменных правил в контроллере
}
}
// ── Тот же домен, другой контроллер (Kafka-потребитель) ─────────
@KafkaListener(topics = "order-requests")
class PlaceOrderEventController {private final PlaceOrderService service;
void handle(PlaceOrderEvent event) {service.placeOrder(event.toCommand()); // тот же сервис, другая точка входа
}
}
Паттерн Полиморфизм адресует поведенческую вариативность, зависящую от типа. Когда вы видите switch или if/else по полю типа — это обычно сигнал применить паттерн: каждая ветка условного оператора должна стать подтипом, а поведение должно диспетчеризоваться системой типов, а не вызывающим кодом. Это прямая связь с OCP: switch растёт с каждым новым типом; полиморфизм означает добавление нового типа без изменения существующего кода.
// ── НАРУШЕНИЕ: switch по типу, растущий с каждой новой скидкой ──
Money applyDiscount(Order order, DiscountType type) { return switch (type) {case PERCENTAGE -> order.total().multiply(0.9);
case FIXED -> order.total().subtract(Money.of(10));
case BOGO -> order.total().multiply(0.5); // добавлено позже — метод изменён
};
}
// ── ИСПРАВЛЕНИЕ: каждый тип инкапсулирует своё поведение ─────────
interface DiscountPolicy { Money apply(Money total); }record PercentageDiscount(double rate) implements DiscountPolicy { public Money apply(Money total) { return total.multiply(1 - rate); }}
record FixedDiscount(Money amount) implements DiscountPolicy { public Money apply(Money total) { return total.subtract(amount); }}
record BogoDiscount() implements DiscountPolicy { public Money apply(Money total) { return total.multiply(0.5); }}
// Вызывающий код — не меняется при добавлении новых типов скидок:
Money discounted = policy.apply(order.total());
Ларман назвал Устойчивость к изменениям «наиболее фундаментальным принципом в разработке ПО» — и сложно с этим поспорить. Каждый паттерн проектирования, каждый архитектурный стиль, каждая абстракция существуют для того, чтобы защитить стабильный код от нестабильных изменений. Практическое применение: выявляйте точки нестабильности вашей системы (внешние API, часто меняющиеся бизнес-правила, технология базы данных, сторонние библиотеки) и оборачивайте каждую стабильным интерфейсом. Код, зависящий от интерфейса, защищён — он не изменится при изменении нестабильного за ним.
Искусственная сущность разрешает противоречие между высокой связностью и необходимостью где-то разместить инфраструктурные задачи. Рассмотрим персистентность: сущность Order не должна знать, как сохранить себя в базу данных — это нарушило бы SRP и внесло инфраструктуру в домен. Но логика персистентности должна где-то жить. Repository — искусственная сущность: класс, изобретённый ради проектирования, не имеющий прямого аналога в предметной области. Он существует для того, чтобы дать поведению персистентности высокосвязный, слабосвязанный дом. Другие примеры: Mapper, Logger, EventPublisher, CacheManager — все они искусственные сущности.
| Паттерн GRASP | Соответствующий паттерн GoF | Что делает на практике |
|---|---|---|
| Информационный эксперт | — (принцип, не паттерн) | Метод живёт в классе с данными |
| Создатель | Factory Method, Abstract Factory | Создание объекта рядом с контекстом использования |
| Контроллер | Facade, Command | Единая точка входа для системных событий |
| Слабое зацепление | Facade, Mediator, Adapter | Снижает поверхность зависимостей между модулями |
| Высокая связность | — (принцип, не паттерн) | Связанное поведение в одном классе |
| Полиморфизм | Strategy, State, Command | Диспетчеризация по типу вместо условных операторов |
| Искусственная сущность | Repository, Mapper, Logger | Изобретённый класс для инфраструктурного поведения |
| Косвенность | Adapter, Proxy, Decorator, Bridge | Промежуточный объект поглощает связанность |
| Устойчивость к изменениям | Все паттерны — это корень | Стабильный интерфейс вокруг точки нестабильности |