← назад к разделу

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 существует в явном виде.