GRASP (General Responsibility Assignment Software Patterns) — девять принципов из книги Крейга Лармана «Applying UML and Patterns». Все девять отвечают на один вопрос, который встаёт при каждом новом методе: какому классу отдать эту ответственность?
SOLID описывает, каким должен быть класс; GRASP — кто за что отвечает. Это два взгляда на одно и то же проектирование, и GRASP из них более практичный: «куда положить этот метод» — вопрос, который возникает десять раз на дню.
Information Expert
Ответственность — тому, у кого есть данные для её выполнения.
Самый главный принцип из девяти. Вычисление суммы заказа делает заказ — у него строки и цены:
public class Order {
private final List<OrderLine> lines;
public Money total() {
return lines.stream()
.map(OrderLine::subtotal)
.reduce(Money.ZERO, Money::add);
}
}
Массовое нарушение Information Expert имеет имя — anemic domain model: данные лежат в Order, а вся логика — в OrderService.calculateTotal(order), который дёргает геттеры. Класс с данными стал пассивной структурой, класс с логикой не имеет данных, и любое изменение трогает оба.
В Spring-сервисе принцип решает вечный спор «логика в сервисе или в домене»: если для решения нужны только данные агрегата — метод принадлежит агрегату; сервису остаётся то, что требует внешних зависимостей.
Creator
Объект A создаёт тот, кто содержит A, агрегирует A или владеет данными для его инициализации.
OrderLine создаёт Order — он содержит строки и владеет контекстом:
public class Order {
public void addLine(Product product, int quantity) {
ensureStatus(Status.CREATED);
lines.add(OrderLine.of(product.id(), product.price(), quantity));
}
}
Контроллер, создающий OrderLine напрямую и складывающий его в заказ через сеттер, нарушает Creator — и заодно теряет инвариант (ensureStatus никто не вызвал).
В Spring глобальный Creator — контейнер: бины создаёт тот, кто ими владеет, ApplicationContext, по рецептам из @Configuration. Прикладному коду остаётся доменное правило: агрегат создаёт свои внутренние сущности, фабричный метод создаёт агрегат.
Controller
Системную операцию принимает не UI-класс, а выделенный объект-координатор: контроллер use case-а.
GRASP Controller — это не @RestController. REST-контроллер — транспортная граница: распарсить HTTP, смаппить DTO, вернуть статус. GRASP Controller — тот, кто координирует выполнение операции. В UCP он существует в явном виде:
@RestController
@RequiredArgsConstructor
public class OrderController {
private final UseCaseDispatcher dispatcher;
@PostMapping("/v1/orders/{orderId}/cancel")
public void cancel(@PathVariable UUID orderId, @RequestBody @Valid CancelOrderRequest request) {
dispatcher.dispatch(new CancelOrderUseCase(new OrderId(orderId), request.reason()));
}
}
@Component
@RequiredArgsConstructor
public class CancelOrderUseCaseHandler implements UseCaseHandler<Void, CancelOrderUseCase> {
@Override
@Transactional
public Void handle(CancelOrderUseCase useCase) { ... }
}
Handler — буквальная реализация GRASP Controller: один на use case, владеет транзакцией, координирует агрегат и порты. Когда такой роли в системе нет, координация расползается по REST-контроллерам — и бизнес-логика становится недостижимой ни из Kafka-листенера, ни из теста без MockMvc.
Low Coupling
Из равных вариантов выбирай тот, что создаёт меньше связей между классами.
Связь — это всё, что заставит класс измениться вслед за другим: вызов, тип поля, наследование. Spring снижает coupling всей своей механикой: инжекция интерфейса вместо new, события вместо прямого вызова, @ConfigurationProperties вместо разбросанных @Value.
Практическая мера в UCP-сервисе: Handler зависит от портов (интерфейсов), порты — от домена, и никто не зависит от адаптеров. Side-effect-ы — через domain events, а не через инжекцию пятнадцати сервисов в один Handler: пятнадцать полей в конструкторе — диагноз high coupling, который виден без инструментов.
High Cohesion
Класс делает близкие по смыслу вещи; несвязанные ответственности — в разных классах.
Зеркало Low Coupling: связи между классами минимальны, связность внутри класса максимальна. Толстый OrderService на восемьсот строк — низкая связность (создание, отмена, экспорт и статистика не связаны между собой); CancelOrderUseCaseHandler — высокая: всё в нём служит одной операции.
На уровне пакетов тот же принцип решает спор «по слоям или по фичам»: пакет ru.shop.order со всем, что про заказ, связнее, чем пакет ru.shop.services со всеми сервисами всех доменов.
Polymorphism
Поведение, зависящее от типа, — полиморфизму, а не условным операторам по типу.
switch по типу клиента, способу оплаты, формату экспорта — кандидат на полиморфную замену: интерфейс + реализации, собираемые контейнером. Развёрнутый пример — DiscountPolicy в статье про SOLID; механика — Strategy из каталога GoF.
Современная оговорка: для закрытого множества вариантов sealed interface + switch с pattern matching — равноправная альтернатива. Компилятор проверяет полноту веток, а реализации не размазаны по классам. Полиморфизм выигрывает, когда множество вариантов открыто — плагины, новые тарифы, новые провайдеры.
Pure Fabrication
Класс-выдумка, не представляющий доменное понятие, — допустим, если он улучшает cohesion и coupling.
В предметной области нет понятия «репозиторий» — покупатель не говорит «положи заказ в OrderRepository». Но без этой выдумки логика хранения размазалась бы по домену. OrderRepository, OrderDtoMapper, Clock, UuidGenerator — всё это Pure Fabrication: классы, придуманные ради чистоты дизайна.
Принцип легализует то, что иначе казалось бы нарушением Information Expert: сохранение заказа не отдают самому заказу (хотя данные у него), потому что тогда домен сцепился бы с jOOQ. Выдуманный класс-посредник дешевле этой связи.
Indirection
Посредник между двумя элементами снимает прямую связь между ними.
ApplicationEventPublisher — посредник между публикатором и слушателями; DispatcherServlet — между HTTP и обработчиками; порт — между Handler-ом и внешней системой. Всякий раз два класса, которые могли бы знать друг о друге, знают только посредника.
Классика жанра: «нет проблемы, которую нельзя решить дополнительным уровнем косвенности, — кроме проблемы слишком многих уровней косвенности». Посредник оправдан, когда развязывает то, что должно меняться независимо. Интерфейс с единственной реализацией внутри одного модуля, порт ради порта — это уже налог без выгоды.
Protected Variations
Найди точки вероятных изменений и закрой их стабильным интерфейсом.
Зонтичный принцип, ради которого существуют остальные: предсказать, что будет меняться, и сделать так, чтобы изменение не разошлось волной. Платёжный провайдер сменится — значит, Handler знает PaymentGateway, а не SDK провайдера. Формат события эволюционирует — значит, в нём eventType с версией. Чужой API нестабилен — значит, между ним и доменом anti-corruption layer.
OCP и DIP из SOLID — конкретные техники Protected Variations; гексагональная архитектура — его систематическое применение к целому сервису. Обратная сторона — YAGNI: защищать стоит вероятные изменения, а не все мыслимые.
Девять принципов одной таблицей
| Принцип | Вопрос | Типовой ответ в UCP-сервисе |
|---|---|---|
| Information Expert | У кого данные для этой логики? | Логика агрегата — в агрегате, не в сервисе |
| Creator | Кто создаёт объект? | Агрегат создаёт свои сущности; фабричный метод — агрегат |
| Controller | Кто принимает системную операцию? | UseCase Handler, не REST-контроллер |
| Low Coupling | Какой вариант создаёт меньше связей? | Порты и события вместо прямых зависимостей |
| High Cohesion | Это всё ещё один класс? | Один Handler = одна операция; пакеты по фичам |
| Polymorphism | Поведение зависит от типа? | Стратегии; sealed + switch для закрытых множеств |
| Pure Fabrication | Нужен класс вне домена? | Repository, Mapper, Clock — выдумки ради чистоты |
| Indirection | Развязать двух участников? | Publisher, dispatcher, порт — посредник между ними |
| Protected Variations | Где будущая точка изменений? | Стабильный интерфейс вокруг неё: порт, ACL, версия события |
Что почитать дальше
- SOLID на примерах Spring — каким должен быть класс, которому вы отдали ответственность.
- Паттерны GoF в Spring — готовые конструкции для Polymorphism, Indirection и Pure Fabrication.
- Тактические паттерны DDD — Information Expert и Creator, доведённые до агрегатов.
- Use Case Pattern — методология, в которой GRASP Controller существует в явном виде.