Переключить тему
~/wiki / server-i-logika / grasp-principy-prostymi-slovami

GRASP простыми словами: 9 принципов, которые помогают правильно распределять ответственность в коде

Основной чат

Чат для вайбкодеров: новости, гайды, поиск исполнителей, маркетплейс и разбор реальных кейсов.

$ cd раздел/ $ join vibe dev
GRASP простыми словами: 9 принципов, которые помогают правильно распределять ответственность в коде - обложка

Когда проект только начинается, архитектура кажется простой. Есть форма заказа, кнопка оплаты и несколько запросов к базе данных. Можно попросить ИИ написать OrderService, сложить туда расчёт суммы, создание заказа, оплату, отправку письма и получить рабочий результат за один вечер.

Проблема появляется позже. Чтобы добавить промокод, приходится менять оплату. Чтобы подключить второй платёжный сервис, нужно переписать половину OrderService. Чтобы протестировать расчёт суммы, приходится поднимать базу данных и подменять HTTP-запросы. Один класс знает слишком много, делает слишком много и меняется по слишком многим причинам.

Обычно в этот момент вспоминают SOLID. Но SOLID говорит, какими свойствами должен обладать хороший код, а не всегда объясняет, кому именно поручить новую ответственность.

Именно на этот вопрос отвечает GRASP.

Если совсем коротко, GRASP - это девять принципов распределения ответственности между объектами и модулями. Они помогают решить:

  • кто должен рассчитывать сумму заказа;
  • кто должен создавать новый объект;
  • где принимать запрос от пользователя;
  • как подключать внешние сервисы;
  • как не превратить контроллер или сервис в God Object;
  • где нужна абстракция, а где она только усложнит проект.

GRASP полезен не только в классическом объектно-ориентированном программировании. Те же вопросы возникают в TypeScript, Python, Java, C#, PHP, frontend-приложениях, Telegram-ботах и серверных функциях. Особенно хорошо эти принципы работают при разработке с ИИ: модель быстро генерирует локально правильный код, но без явных границ часто складывает все сценарии в один большой сервис.

Что такое GRASP

GRASP расшифровывается как General Responsibility Assignment Software Patterns: общие паттерны распределения ответственности в программном обеспечении.

Подход описал Крейг Ларман в книге Applying UML and Patterns. В GRASP входят девять принципов:

  1. Information Expert - информационный эксперт.
  2. Creator - создатель.
  3. Controller - контроллер.
  4. Low Coupling - слабая связанность.
  5. High Cohesion - высокая связность или сфокусированность модуля.
  6. Polymorphism - полиморфизм.
  7. Pure Fabrication - чистая выдумка.
  8. Indirection - посредник.
  9. Protected Variations - защита от изменений.

Слово «паттерны» здесь может сбить с толку. GRASP не предлагает готовые конструкции вроде Factory Method или Observer. Это скорее набор вопросов, которые помогают принимать архитектурные решения.

Например, вместо вопроса «в какой файл положить эту функцию?» GRASP предлагает спросить:

Какой объект уже владеет данными, необходимыми для выполнения этой операции?

Ответ часто сразу показывает правильное место для логики.

Сквозной пример: сервис оформления заказа

Представим интернет-магазин. ИИ сгенерировал такой сервис:

ts
class CheckoutService {
  async checkout(input: CheckoutInput) {
    const products = await db.products.findMany({
      where: { id: { in: input.productIds } },
    });

    const subtotal = products.reduce(
      (sum, product) => sum + product.price,
      0,
    );

    const discount = input.promoCode
      ? await db.promocodes.calculate(input.promoCode, subtotal)
      : 0;

    const total = subtotal - discount;

    const payment = await fetch("https://payment.example/charge", {
      method: "POST",
      body: JSON.stringify({ amount: total }),
    });

    const order = await db.orders.create({
      data: {
        email: input.email,
        total,
        paymentId: (await payment.json()).id,
      },
    });

    await emailClient.send({
      to: input.email,
      subject: "Заказ оформлен",
      template: "order-created",
      data: order,
    });

    return order;
  }
}

Код может работать. Но один метод одновременно:

  • загружает товары;
  • рассчитывает сумму;
  • применяет скидку;
  • знает протокол платёжного API;
  • сохраняет заказ;
  • формирует уведомление;
  • управляет всем пользовательским сценарием.

Это типичный результат запроса «сделай оформление заказа целиком». ИИ оптимизирует решение под видимый сценарий, а не под будущие изменения. GRASP позволяет разобрать этот комок ответственности по понятным правилам.

1. Information Expert: поручайте работу тому, кто владеет данными

Information Expert, или «информационный эксперт», - главный принцип GRASP.

Ответственность следует назначать объекту, у которого уже есть данные, необходимые для её выполнения.

Кто должен рассчитывать итоговую сумму заказа? Не контроллер и не репозиторий. Сам заказ уже содержит позиции, цены и количество, поэтому он является информационным экспертом.

ts
type OrderLine = {
  productId: string;
  unitPrice: number;
  quantity: number;
};

class Order {
  constructor(private readonly lines: OrderLine[]) {}

  subtotal(): number {
    return this.lines.reduce(
      (sum, line) => sum + line.unitPrice * line.quantity,
      0,
    );
  }
}

Теперь расчет находится рядом с данными. Если появится количество, налог или округление, не нужно искать формулу в контроллере, SQL-запросе и шаблоне письма.

Практический вопрос Information Expert:

У кого уже есть большая часть данных для этой операции?

Но принцип нельзя применять механически. Объект пользователя знает email, однако это не означает, что он должен самостоятельно открывать SMTP-соединение. Данные и техническая интеграция - разные виды ответственности. В таких случаях понадобятся другие принципы GRASP.

2. Creator: создавать объект должен его естественный владелец

Принцип Creator помогает выбрать, кто должен создавать новый объект.

Объект B является хорошим кандидатом на создание объекта A, если B:

  • содержит или агрегирует A;
  • активно использует A;
  • хранит записи об A;
  • располагает данными, необходимыми для инициализации A.

Заказ содержит позиции заказа, поэтому логично создавать их через сам заказ, а не разбрасывать конструкторы OrderLine по контроллерам.

ts
class Order {
  private readonly lines: OrderLine[] = [];

  addProduct(product: Product, quantity: number): void {
    this.lines.push({
      productId: product.id,
      unitPrice: product.price,
      quantity,
    });
  }

  subtotal(): number {
    return this.lines.reduce(
      (sum, line) => sum + line.unitPrice * line.quantity,
      0,
    );
  }
}

Creator не означает, что любой сложный объект нужно собирать через new внутри доменной модели. Если создание требует базы данных, конфигурации, нескольких зависимостей или сложной валидации, отдельная фабрика может быть лучше.

Главная идея проще: создание объекта должно происходить там, где уже есть контекст для корректной инициализации.

3. Controller: отделяйте вход в систему от бизнес-логики

Controller в GRASP - это объект, который принимает системное событие и передает работу подходящим участникам.

Для HTTP-приложения таким событием может быть POST /checkout. Для Telegram-бота - команда /buy. Для очереди - входящее сообщение OrderRequested.

Хороший контроллер:

  • принимает и проверяет форму входных данных;
  • вызывает сценарий приложения;
  • превращает результат в HTTP-ответ или сообщение;
  • не рассчитывает цены и не реализует правила оплаты.
ts
class CheckoutController {
  constructor(private readonly checkout: CheckoutUseCase) {}

  async handle(request: CheckoutRequest): Promise<CheckoutResponse> {
    const result = await this.checkout.execute({
      customerEmail: request.body.email,
      items: request.body.items,
      paymentMethod: request.body.paymentMethod,
    });

    return {
      status: 201,
      body: result,
    };
  }
}

Контроллер координирует, но не становится владельцем бизнес-правил.

Частая ошибка - создать «тонкий» HTTP-контроллер, который вызывает огромный CheckoutService. Формально контроллер останется маленьким, но God Object просто переедет в другой файл. GRASP нужно применять ко всей цепочке, а не только к transport-слою.

4. Low Coupling: уменьшайте количество знаний о соседях

Low Coupling, или слабая связанность, означает, что модуль должен как можно меньше знать о деталях других модулей.

В исходном примере CheckoutService знает URL платёжного сервиса, формат JSON, способ авторизации и структуру ответа. Любое изменение внешнего API затронет бизнес-сценарий.

Вместо этого сценарий может зависеть от небольшого контракта:

ts
type ChargeResult = {
  transactionId: string;
};

interface PaymentGateway {
  charge(amount: number): Promise<ChargeResult>;
}

class CheckoutUseCase {
  constructor(private readonly paymentGateway: PaymentGateway) {}
}

Конкретная интеграция остается на краю системы:

ts
class AcmePaymentGateway implements PaymentGateway {
  async charge(amount: number): Promise<ChargeResult> {
    const response = await fetch("https://payment.example/charge", {
      method: "POST",
      body: JSON.stringify({ amount }),
    });

    const payload = await response.json();
    return { transactionId: payload.id };
  }
}

Слабая связанность дает практические преимущества:

  • платёжного провайдера можно заменить локально;
  • сценарий легко тестировать без сети;
  • детали внешнего API не расползаются по проекту;
  • изменения становятся предсказуемее.

Но минимальная связанность не является самоцелью. Если ради одного вызова появляется семь интерфейсов и четыре адаптера, цена абстракции может быть выше пользы. Важно изолировать реальные точки изменений, а не каждую строку кода.

5. High Cohesion: один модуль должен решать сфокусированную задачу

Термин High Cohesion часто переводят как «высокая связность». Его легко перепутать со связанностью между модулями. По смыслу речь идет о внутренней цельности: обязанности одного объекта должны быть тесно связаны между собой.

У CheckoutService низкая cohesion, потому что расчёт цены, HTTP-интеграция, сохранение и отправка письма меняются по разным причинам.

После разделения роли могут выглядеть так:

text
CheckoutController  -> принимает HTTP-запрос
CheckoutUseCase     -> координирует сценарий
Order               -> хранит правила заказа и считает сумму
DiscountPolicy      -> рассчитывает скидку
PaymentGateway      -> проводит оплату
OrderRepository     -> сохраняет заказ
OrderNotifier       -> отправляет уведомление

У каждого элемента появляется понятная причина для изменения.

Проверить cohesion можно простым вопросом:

Можно ли описать назначение этого класса одним предложением без слов «а еще»?

Если описание звучит как «оформляет заказ, а еще отправляет письма, пишет аудит и синхронизирует CRM», границы почти наверняка выбраны плохо.

6. Polymorphism: заменяйте ветвления взаимозаменяемыми реализациями

Polymorphism предлагает поручать различающееся поведение самим типам, а не собирать растущий if или switch в одном месте.

Плохой признак:

ts
if (method === "card") {
  // один протокол
} else if (method === "sbp") {
  // другой протокол
} else if (method === "invoice") {
  // третий протокол
}

Каждый новый способ оплаты заставляет менять общий сценарий. Вместо этого можно использовать взаимозаменяемые обработчики:

ts
interface PaymentMethod {
  pay(amount: number): Promise<ChargeResult>;
}

class CardPayment implements PaymentMethod {
  async pay(amount: number): Promise<ChargeResult> {
    // Оплата картой
    return { transactionId: "card-transaction" };
  }
}

class SbpPayment implements PaymentMethod {
  async pay(amount: number): Promise<ChargeResult> {
    // Оплата через СБП
    return { transactionId: "sbp-transaction" };
  }
}

В TypeScript полиморфизм не требует сложной иерархии классов. Часто достаточно интерфейса и нескольких обычных объектов или функций с одинаковым контрактом.

Применять принцип стоит, когда варианты поведения действительно развиваются независимо. Если условие одно, стабильно и никогда не расширяется, отдельная иерархия только затруднит чтение.

7. Pure Fabrication: иногда полезный объект приходится придумать

Не для каждой технической обязанности существует естественный объект предметной области. Кому поручить сохранение заказа? Сам Order не должен знать SQL и структуру таблиц. Покупатель тоже не подходит.

Pure Fabrication, или «чистая выдумка», разрешает создать технический объект, которого нет в реальном бизнесе, если он повышает cohesion и снижает coupling.

Типичные примеры:

  • OrderRepository;
  • EmailNotifier;
  • PaymentGateway;
  • UserMapper;
  • AuditLogger.
ts
interface OrderRepository {
  save(order: Order): Promise<void>;
}

class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> {
    // Преобразование модели и запись в PostgreSQL
  }
}

Репозиторий - не объект бизнеса интернет-магазина. Он придуман разработчиком, чтобы доменная модель не зависела от PostgreSQL.

Опасность Pure Fabrication - бесконтрольное размножение сервисов с расплывчатыми названиями: CommonService, HelperManager, UtilsProvider. Выдуманный объект полезен только тогда, когда у него есть четкая техническая ответственность.

8. Indirection: добавляйте посредника между тем, что не должно зависеть напрямую

Indirection предлагает ввести промежуточный объект, чтобы два компонента не знали друг о друге напрямую.

Пример: после создания заказа нужно отправить письмо, записать событие в аналитику и передать данные в CRM. Если CheckoutUseCase напрямую вызывает все три системы, он быстро обрастет зависимостями.

Посредником может стать диспетчер событий:

ts
type OrderCreated = {
  orderId: string;
  customerEmail: string;
};

interface EventBus {
  publish(event: OrderCreated): Promise<void>;
}

class CheckoutUseCase {
  constructor(private readonly events: EventBus) {}

  async execute(input: CheckoutInput): Promise<void> {
    // Создание и сохранение заказа
    await this.events.publish({
      orderId: "order-id",
      customerEmail: input.customerEmail,
    });
  }
}

Подписчики могут независимо отправить письмо, обновить аналитику и синхронизировать CRM.

Посредник снижает прямую связанность, но добавляет новый маршрут выполнения. Событийная схема усложняет отладку, обработку ошибок и гарантии доставки. Для небольшого приложения обычный вызов OrderNotifier может быть понятнее.

Indirection нужен не везде. Его цена оправдана, когда прямая зависимость действительно мешает изменениям или повторному использованию.

9. Protected Variations: ставьте стабильную границу вокруг нестабильной части

Protected Variations - принцип защиты от изменений. Сначала нужно найти часть системы, которая с высокой вероятностью будет меняться, а затем окружить её стабильным контрактом.

Чаще всего меняются:

  • платёжные и логистические API;
  • поставщики email и SMS;
  • структура внешних webhook;
  • модели и провайдеры ИИ;
  • базы данных и облачные хранилища;
  • правила скидок и тарифов.

Если проект обращается к OpenAI, Anthropic или локальной модели напрямую из десятков файлов, замена провайдера станет дорогой. Стабильная граница локализует изменение:

ts
interface TextGenerator {
  generate(prompt: string): Promise<string>;
}

class OpenAiTextGenerator implements TextGenerator {
  async generate(prompt: string): Promise<string> {
    // Детали OpenAI API находятся только здесь
    return "generated text";
  }
}

Это особенно важно для AI-интеграций. У провайдера могут измениться модель, формат tool calls, лимиты, схема ошибок или политика ретраев. Бизнес-логика не должна знать каждую такую деталь.

Protected Variations похож на Low Coupling, но делает акцент на прогнозируемой точке нестабильности. Не нужно защищать абстракцией всё подряд. Нужно защищать то, что действительно меняется или находится вне вашего контроля.

Как девять принципов складываются в одну архитектуру

После применения GRASP оформление заказа может выглядеть так:

text
HTTP request
    |
CheckoutController          Controller
    |
CheckoutUseCase             High Cohesion
    |-- Order               Information Expert + Creator
    |-- DiscountPolicy      Polymorphism
    |-- PaymentGateway      Protected Variations
    |-- OrderRepository     Pure Fabrication
    `-- EventBus            Indirection

Все зависимости между частями стараемся держать слабыми: Low Coupling.

GRASP не выдает единственно правильную схему. Он помогает аргументировать решение:

  • сумма находится в Order, потому что заказ владеет позициями;
  • HTTP-контроллер только принимает системное событие;
  • платёжный API скрыт за стабильным контрактом;
  • репозиторий создан как техническая абстракция;
  • разные способы оплаты реализуют общее поведение;
  • внешние реакции можно отделить через посредника.

Так архитектура перестает быть набором вкусовых предпочтений. Для каждого решения появляется проверяемая причина.

GRASP и SOLID: в чем разница

GRASP и SOLID не конкурируют. Они работают на разных уровнях и дополняют друг друга.

GRASP SOLID
Помогает назначать ответственность Помогает оценивать качество границ и зависимостей
Спрашивает: «кто должен это делать?» Спрашивает: «насколько легко это менять и расширять?»
Ориентирован на проектирование взаимодействий Ориентирован на устойчивость модулей и контрактов
Содержит Information Expert, Creator, Controller Содержит SRP, OCP, LSP, ISP, DIP
Часто применяется при первом разбиении сценария Часто применяется при проверке и рефакторинге решения

Практический порядок может быть таким:

  1. С помощью GRASP определить, кому принадлежит каждая ответственность.
  2. С помощью SOLID проверить, не получились ли слишком широкие роли и хрупкие зависимости.
  3. С помощью чистой архитектуры определить направление зависимостей между крупными слоями.

Подробный разбор пяти принципов уже есть в статье «SOLID простыми словами».

Как применять GRASP к коду, который написал ИИ

ИИ редко нарушает архитектуру одной очевидной ошибкой. Чаще он постепенно расширяет уже существующий сервис: добавляет ещё одну зависимость, ещё один if, ещё один запрос к базе и ещё одну техническую обязанность.

Поэтому запрос «отрефактори по GRASP» слишком расплывчатый. Лучше заставить агента сначала объяснить распределение ответственности.

Рабочий промпт:

text
Проанализируй этот сценарий до изменения кода.

1. Перечисли все ответственности текущих модулей.
2. Для каждой ответственности укажи Information Expert.
3. Определи объект Controller для системного события.
4. Найди модули с низкой cohesion и слишком сильным coupling.
5. Найди ветвления, которые лучше заменить полиморфизмом.
6. Отметь нестабильные внешние интеграции для Protected Variations.
7. Предложи минимальное изменение структуры без лишних абстракций.
8. Только после согласования плана меняй код и добавляй тесты.

Такой запрос полезнее, чем просьба «сделай архитектуру красивой». Он требует от модели показать причинно-следственную связь и позволяет заметить переусложнение до генерации десятков файлов.

GRASP также можно добавить в проектные инструкции агента:

md
Перед добавлением новой бизнес-логики определи владельца ответственности:
- данные и вычисления размещай у Information Expert;
- transport-контроллеры оставляй тонкими;
- изолируй изменчивые внешние API стабильными контрактами;
- не добавляй интерфейс или посредника без конкретной точки изменения;
- явно сообщай, если существующий модуль теряет cohesion.

Когда GRASP превращается в переусложнение

GRASP - не требование создавать отдельный класс для каждой функции.

Для маленького скрипта, одноразовой миграции или простого CRUD-сценария прямой код может быть лучшим решением. Архитектурная конструкция оправдана, когда она уменьшает стоимость ожидаемых изменений.

Тревожные признаки перегиба:

  • интерфейс имеет только одну реализацию, а точки изменения не видно;
  • простое действие проходит через пять посредников;
  • классы состоят из одного метода и не владеют состоянием или правилами;
  • названия вроде Manager, Processor, Handler скрывают неясную ответственность;
  • для понимания сценария приходится открывать десять файлов;
  • тесты проверяют моки, а не бизнес-поведение.

GRASP нужно использовать как набор эвристик, а не как генератор слоев. Хорошее решение остается достаточно простым, чтобы новый разработчик мог пройти по сценарию от входа до результата.

Практический чек-лист GRASP для ревью

Перед добавлением новой функции или во время ревью задайте девять вопросов:

  1. Information Expert: у какого объекта уже есть данные для этой операции?
  2. Creator: кто естественно содержит, использует или инициализирует создаваемый объект?
  3. Controller: кто принимает системное событие и где заканчивается transport-логика?
  4. Low Coupling: сколько чужих деталей знает этот модуль?
  5. High Cohesion: можно ли описать его роль одним предложением без «а еще»?
  6. Polymorphism: растет ли switch при добавлении каждого нового варианта?
  7. Pure Fabrication: нужна ли отдельная техническая сущность для хранения, отправки или преобразования?
  8. Indirection: мешает ли прямая зависимость изменению или тестированию компонентов?
  9. Protected Variations: какая часть наиболее нестабильна и где поставить стабильную границу?

Не обязательно применять все девять принципов в каждой задаче. Ценность чек-листа в том, что архитектурные решения становятся осознанными, а не случайными.

Частые вопросы

Что такое GRASP простыми словами?

GRASP - это девять принципов, которые помогают решить, какой объект или модуль должен отвечать за конкретное действие. Они уменьшают связанность, сохраняют сфокусированность классов и защищают проект от изменений.

Сколько принципов входит в GRASP?

В классический набор входят девять принципов: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection и Protected Variations.

Чем GRASP отличается от SOLID?

GRASP помогает распределить обязанности между объектами, а SOLID помогает проверить устойчивость получившихся модулей и зависимостей. GRASP чаще отвечает на вопрос «кто должен это делать?», SOLID - «как сделать это изменяемым и расширяемым?».

Можно ли применять GRASP в функциональном программировании?

Да. Хотя терминология GRASP появилась в объектно-ориентированном проектировании, идеи информационного эксперта, слабой связанности, высокой cohesion, посредников и защиты от изменений применимы к функциям, модулям и сервисам.

Нужен ли GRASP для небольшого проекта?

Для небольшого проекта достаточно использовать GRASP как чек-лист. Не нужно заранее строить сложную архитектуру. Сначала определите владельцев бизнес-правил и изолируйте действительно изменчивые внешние зависимости.

Почему GRASP полезен при разработке с ИИ?

ИИ хорошо реализует отдельные функции, но часто помещает новые обязанности в ближайший существующий сервис. GRASP дает конкретные вопросы для проверки результата и помогает не допустить появления God Object по мере роста проекта.

Главный вывод

Большинство архитектурных проблем начинается не с плохого синтаксиса и не с неправильного фреймворка. Они начинаются в момент, когда новая ответственность попадает не к тому владельцу.

GRASP предлагает практичный способ принимать такие решения:

  • данные подсказывают информационного эксперта;
  • естественный владелец создает вложенные объекты;
  • контроллер принимает событие, но не присваивает себе бизнес-логику;
  • внешние и изменчивые детали получают стабильные границы;
  • модули остаются сфокусированными и знают друг о друге только необходимое.

Не нужно внедрять все девять паттернов одновременно. Начните с трех вопросов: кто владеет данными, кто принимает системное событие и какая часть системы будет меняться чаще всего. Уже этого достаточно, чтобы код, написанный человеком или ИИ, рос заметно предсказуемее.


Основной источник: Craig Larman, Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development.

$ cd ../ ← назад к Сервер и логика