GRASP: как правильно распределять ответственность между объектами
Sorry for my English! English is not my native language. One of the reasons to create this blog is to improve my English writing. So I will be highly obliged if you will help me with this. If you find a grammar error on this page, please select it with your mouse and press Ctrl+Enter.
Хочу рассказать вам о принципах GRASP. Если вы уже знакомы с ООП и особенно с SOLID, то некоторые идеи из GRASP наверняка покажутся вам знакомыми. Возможно, вы даже обнаружите, что давно используете их в своей работе, просто не знали, что у этих решений есть конкретные названия.
Я сам пришёл к GRASP именно с таким ощущением. Часть принципов оказалась довольно очевидной, потому что я уже применял похожие подходы на практике. Но некоторые идеи заставили меня по-другому посмотреть на проектирование классов и распределение ответственности между ними.
И, на мой взгляд, это как раз главная ценность GRASP.
GRASP не столько говорит нам «пиши код вот так», сколько помогает ответить на гораздо более важный вопрос:
Кому должна принадлежать конкретная ответственность?
И это тот вопрос, который мы постоянно задаём себе при проектировании программы.
Что такое GRASP
GRASP расшифровывается как General Responsibility Assignment Software Patterns — общие шаблоны распределения ответственности в программном обеспечении.
GRASP состоит из девяти принципов:
- Information Expert — информационный эксперт
- Creator — создатель
- Controller — контроллер
- Low Coupling — низкая связанность
- High Cohesion — высокая связность
- Polymorphism — полиморфизм
- Pure Fabrication — чистая выдумка
- Indirection — посредничество
- Protected Variations — защищённые вариации
На первый взгляд это может выглядеть как очередной список правил, который нужно просто выучить.
Но я бы так делать не советовал.
Мне гораздо больше нравится воспринимать GRASP как набор линз, через которые мы смотрим на одну и ту же задачу:
У нас появилась ответственность. Кому её передать?
Не создаст ли это слишком сильную зависимость?
Не станет ли класс слишком большим?
Что будет, если эта часть системы изменится?
Нужен ли здесь отдельный объект или абстракция?
И вот здесь GRASP становится действительно полезным.
GRASP и SOLID
Но между ними очень тесная связь.
SOLID в основном формулирует принципы, которым должен соответствовать хороший объектно-ориентированный дизайн.
Например:
- класс должен иметь одну причину для изменения;
- зависимости лучше направлять на абстракции;
- реализации должны быть взаимозаменяемыми;
- высокоуровневый код не должен зависеть напрямую от деталей.
GRASP больше помогает разобраться, как принимать конкретные решения при проектировании.
Например, у нас есть:
class Order { // ... }
и появляется новая ответственность:
Нужно рассчитать общую стоимость заказа.
Куда поместить этот код?
GRASP предлагает начать с вопроса:
Кто обладает необходимой информацией?
Если Order содержит позиции заказа, цены и количество, то логично предположить, что именно Order должен уметь рассчитывать свою сумму.
Это Information Expert.
А дальше мы можем проверить решение другими принципами:
- не создаёт ли оно сильную связанность;
- сохраняет ли класс высокую cohesion;
- не появляется ли лишняя зависимость.
То есть GRASP помогает рассуждать о дизайне, а SOLID даёт нам набор фундаментальных критериев, которым этот дизайн должен соответствовать.
При этом между ними много пересечений.
Например:
OrderService
↓
PaymentGateway
↑
│
StripeGateway
Одно и то же решение можно рассматривать сразу с нескольких сторон:
Polymorphism:
StripeGateway и PayPalGateway реализуют общий контракт.
Protected Variations:
Мы защищаем OrderService от изменения конкретного платёжного провайдера.
Indirection:
PaymentGateway находится между OrderService и конкретной реализацией.
Low Coupling:
OrderService больше не зависит напрямую от Stripe.
И это нормально. GRASP-принципы не являются взаимоисключающими.
1. Information Expert — информационный эксперт
Это, пожалуй, один из самых интуитивных принципов.
Идея простая:
Ответственность следует отдавать тому объекту, который обладает необходимой для неё информацией.
Допустим, у нас есть:
class Order { private array $items; }
Каждый OrderItem содержит цену и количество.
Нам нужно получить общую стоимость заказа.
Где это сделать?
Можно создать:
class OrderService { public function calculateTotal(Order $order) { // ... } }
Но почему именно OrderService?
Сам Order уже обладает всей необходимой информацией:
Order
├── OrderItem
│ ├── price
│ └── quantity
├── OrderItem
│ ├── price
│ └── quantity
└── ...
Поэтому вполне естественно:
$order->total();
Order является Information Expert.
При этом принцип не означает:
«Если класс имеет какие-то данные, вся работа с этими данными обязательно должна находиться в этом классе».
Это было бы слишком примитивным применением принципа.
Например, Order может знать адрес доставки и вес заказа. Но это ещё не означает, что он должен сам обращаться к API службы доставки.
Здесь уже появляются другие вопросы:
не увеличиваем ли мы coupling;
не падает ли cohesion;
не лучше ли создать отдельный сервис или gateway.
То есть Information Expert — это отправная точка для поиска ответственного объекта, а не железное правило.
2. Creator — создатель
Следующий вопрос:
Кто должен создавать объект?
Например, Order состоит из OrderItem.
Кто должен создавать OrderItem?
$order->addItem($product, $quantity);
Здесь Order является хорошим кандидатом на создание OrderItem, потому что:
- Order содержит OrderItem;
- Order использует его;
- Order агрегирует эти объекты;
- OrderItem логически является частью Order.
То есть между объектами уже существует тесная связь.
Важно понимать, что Creator не означает «обязательно написать new внутри этого класса».
Например, создание объекта может происходить через фабрику или контейнер:
$order->addItem(...);
а внутри уже может использоваться другой механизм.
Главная идея Creator — определить, какой объект логично сделать ответственным за создание другого объекта.
В Laravel это особенно интересно, потому что создание зависимостей часто передаётся контейнеру. Поэтому Creator — это не обязательно буквально:
$item = new OrderItem();
Здесь важнее сама ответственность за создание, а не конкретная синтаксическая конструкция.
3. Controller — контроллер
Здесь легко возникнуть путанице с MVC.
В Laravel мы привыкли к Controller примерно такого вида:
class OrderController { public function store(StoreOrderRequest $request) { // ... } }
Но GRASP Controller и MVC Controller — не совсем одно и то же понятие.
В GRASP Controller — это объект, который принимает системное событие от внешнего мира и передаёт его дальше.
Например:
HTTP request
↓
OrderController
↓
OrderService
↓
Domain
Controller не должен превращаться в место, где находится вся бизнес-логика:
public function store(...) { // validate // calculate price // create order // save order // call payment API // send email // ... }
Такой контроллер быстро превращается в God Object.
Его задача — принять запрос и инициировать нужную системную операцию.
Например:
public function store(StoreOrderRequest $request) { return $this->orderService->create( $request->validated() ); }
То есть Controller — это точка входа в систему, а не место, куда нужно складывать всю бизнес-логику.
4. Low Coupling — низкая связанность
Coupling показывает, насколько сильно один компонент зависит от других компонентов.
Например:
class OrderService { public function create(Order $order) { $stripe = new StripePayment(); $stripe->pay($order); } }
Теперь:
OrderService → StripePayment
OrderService напрямую знает о Stripe.
Если мы захотим использовать PayPal, придётся менять OrderService.
Лучше:
class OrderService { public function __construct( private PaymentGateway $paymentGateway ) {} public function pay(Order $order) { return $this->paymentGateway->pay($order); } }
Теперь:
OrderService
↓
PaymentGateway
↑
┌────┴─────┐
Stripe PayPal
OrderService больше не зависит от конкретного провайдера.
Но здесь важно не перепутать Dependency Injection и Low Coupling.
DI — это механизм передачи зависимости.
А Low Coupling — дизайнерская цель:
Не создавать лишних и слишком сильных связей между компонентами.
DI может помочь этой цели, но сам по себе ещё не гарантирует низкую связанность.
5. High Cohesion — высокая связность
Если Low Coupling смотрит на связи между объектами, то Cohesion смотрит на то, что происходит внутри объекта.
Простой вопрос:
Насколько хорошо обязанности этого класса связаны между собой?
Например:
class OrderService { public function calculateTotal() {} public function saveOrder() {} public function sendEmail() {} public function generatePdf() {} public function resizeProductImage() {} public function callPaymentApi() {} }
Формально всё это может иметь какое-то отношение к заказу.
Но класс занимается слишком разными вещами.
У него низкая cohesion.
Гораздо логичнее разделить:
Order
└── total()
OrderRepository
└── save()
OrderMailer
└── send()
InvoiceGenerator
└── generate()
PaymentService
└── pay()
Теперь каждый компонент занимается логически связанной группой задач.
И здесь очень хорошо видно отношение GRASP к SOLID.
High Cohesion очень близок к идее Single Responsibility Principle.
Но я бы не воспринимал их как абсолютно одинаковые формулировки. Они смотрят на дизайн немного с разных сторон.
6. Polymorphism — полиморфизм
Этот принцип знаком практически каждому разработчику.
Представим, что у нас есть разные способы оплаты:
Stripe
PayPal
LiqPay
Fondy
Мы можем создать общий контракт:
interface PaymentGateway { public function pay(Order $order): PaymentResult; }
И разные реализации:
class StripeGateway implements PaymentGateway { // ... } class PayPalGateway implements PaymentGateway { // ... }
Теперь код, который использует оплату, может работать с абстракцией:
PaymentGateway $gateway
и ему не нужно знать, какая конкретно реализация используется.
Именно здесь работает Polymorphism.
Разные объекты имеют общий контракт, но каждый по-своему реализует поведение.
И здесь снова появляется связь с SOLID — прежде всего с Open/Closed Principle и Dependency Inversion Principle.
Например:
PaymentGateway
↑
┌─────┴─────┐
Stripe PayPal
Мы можем добавить новую реализацию, не переписывая код, который работает с PaymentGateway.
Но важно помнить:
Полиморфизм не означает, что для каждого класса нужно создавать интерфейс.
Если у нас есть только один вариант и никаких реальных причин ожидать вариаций, интерфейс может только усложнить код.
7. Pure Fabrication — чистая выдумка
Этот принцип мне показался одним из самых интересных.
Он отвечает на вопрос:
Что делать, если ни один объект предметной области не является хорошим местом для этой ответственности?
Тогда мы можем создать искусственный программный объект, которого вообще нет в реальном бизнесе.
Например:
Order
Customer
Product
Payment
Это реальные сущности нашего магазина.
Но нам нужно сохранять Order в базу данных.
Можно было бы сделать:
class Order { public function save() { // SQL } }
Но тогда Order начинает отвечать и за бизнес-логику, и за persistence.
Поэтому мы можем создать:
class OrderRepository { public function save(Order $order) { // ... } }
В реальном магазине объекта OrderRepository не существует.
Мы его выдумали специально для программы.
И это не недостаток. Наоборот, это может быть хорошим архитектурным решением.
То же самое касается таких классов, как:
PaymentService
OrderMailer
InvoiceGenerator
FreeDeliveryService
OrderRepository
Их может не существовать в предметной области, но они могут прекрасно существовать в программной модели.
При этом Pure Fabrication не означает:
«Давайте вынесем всё в Service».
Если Order естественно может выполнить какую-то операцию самостоятельно, не обязательно создавать для неё отдельный сервис.
Именно поэтому Pure Fabrication хорошо работает вместе с Information Expert:
Сначала ищем естественное место для ответственности. Если такого места нет или решение приводит к плохому дизайну — создаём отдельный программный объект.
8. Indirection — посредничество
Название этого принципа сначала может показаться странным.
Идея очень простая:
Если два объекта слишком сильно завязаны друг на друга, можно поставить между ними посредника.
Вместо:
A → B
получаем:
A → C → B
Например:
OrderService → Stripe
можно заменить на:
OrderService → PaymentGateway → Stripe
PaymentGateway становится посредником.
Или можно использовать события:
OrderService
↓
OrderCreated
↓
Listener
↓
Email
Теперь OrderService не знает, что после создания заказа кто-то должен отправить email.
На первый взгляд добавление промежуточного объекта усложняет систему.
Но иногда именно это позволяет уменьшить связанность и изолировать компоненты друг от друга.
И здесь опять возникает пересечение с другими GRASP-принципами.
Один и тот же PaymentGateway может одновременно быть:
- Indirection — посредником;
- Polymorphism — общим контрактом для разных реализаций;
- Protected Variations — защитой от изменения платёжного провайдера;
- Low Coupling — способом уменьшить прямую зависимость.
Это нормально.
GRASP — не девять независимых коробочек.
9. Protected Variations — защищённые вариации
Этот принцип, на мой взгляд, один из самых интересных для понимания архитектуры.
Основная идея:
Найди место, которое может измениться, и постарайся защитить остальную систему от этой вариации.
Например, мы используем Stripe:
OrderService → Stripe
Сегодня Stripe.
Завтра бизнес говорит:
Нужно добавить PayPal.
Потом:
Ещё LiqPay.
Потом:
Добавляем Fondy.
Если конкретные платёжные системы напрямую используются по всему приложению, изменения начинают распространяться по коду.
Поэтому мы выделяем точку вариации:
Payment Provider
и создаём вокруг неё стабильную абстракцию:
interface PaymentGateway { public function pay(Order $order): PaymentResult; }
Получаем:
Stripe
↑
OrderService → PaymentGateway
↑
PayPal
Теперь OrderService защищён от изменения конкретного платёжного провайдера.
Это и есть Protected Variations.
Здесь полезно различать два понятия.
Point of Variation — место, где существует или ожидается изменение.
Например:
Stripe / PayPal / LiqPay
Point of Protection — место, через которое мы изолируем это изменение:
PaymentGateway
И здесь опять возникает связь с SOLID, особенно с Dependency Inversion.
Но мотивация немного разная.
DIP говорит:
Не заставляй высокоуровневый код зависеть напрямую от деталей.
Protected Variations заставляет нас сначала спросить:
Что в системе потенциально нестабильно и от чего мне нужно защитить остальной код?
Как все эти принципы работают вместе
Мне кажется, именно здесь начинается самое интересное.
Допустим, у нас есть интернет-магазин и операция оплаты заказа.
Мы можем начать рассуждать следующим образом.
Шаг 1. Кто должен знать, как рассчитывается сумма?
Order.
Это Information Expert.
$order->total();
Шаг 2. Кто создаёт OrderItem?
Order, потому что OrderItem является его частью.
Это Creator.
Шаг 3. Кто принимает HTTP-запрос?
OrderController.
Это Controller.
Шаг 4. Кто сохраняет заказ?
Можно создать OrderRepository.
Это Pure Fabrication.
Шаг 5. Как не связать OrderService напрямую со Stripe?
Использовать PaymentGateway.
Это помогает добиться Low Coupling.
Шаг 6. Как не сделать Order огромным?
Не складывать в него persistence, email, интеграции и прочую инфраструктуру.
Здесь помогает High Cohesion.
Шаг 7. Как работать с Stripe и PayPal одинаковым способом?
Использовать общий контракт и разные реализации.
Это Polymorphism.
Шаг 8. Как отделить OrderService от конкретного payment provider?
Использовать промежуточную абстракцию.
Это Indirection.
Шаг 9. Что если Stripe изменится или появится новый провайдер?
Защитить application code стабильным PaymentGateway.
Это Protected Variations.
И в результате мы получаем не девять отдельных решений, а одну связанную систему.
GRASP лучше не учить как таблицу
Можно, конечно, сделать такую шпаргалку:
Принцип Главный вопрос
Information Expert Кто обладает нужной информацией?
Creator Кто должен создавать объект?
Controller Кто принимает системную операцию?
Low Coupling Как уменьшить лишние зависимости?
High Cohesion Насколько логично распределены обязанности внутри класса?
Polymorphism Как реализовать разные варианты поведения?
Pure Fabrication Нужно ли создать искусственный объект для ответственности?
Indirection Нужен ли посредник между компонентами?
Protected Variations От какого изменения нужно защитить систему?
Но мне кажется, что запоминать только эту таблицу мало.
Гораздо полезнее привыкнуть задавать себе вопросы при проектировании:
Кто должен отвечать за эту операцию?
↓
Кто обладает необходимой информацией?
↓
Не создаст ли это слишком сильную зависимость?
↓
Не станет ли класс слишком большим?
↓
Есть ли здесь часть системы, которая может измениться?
↓
Нужно ли защитить от неё остальной код?
↓
Нужен ли здесь отдельный объект, абстракция или посредник?
Вот это уже и есть практическое применение GRASP.
Почему GRASP мне кажется полезным, если уже есть SOLID
Если я уже знаю SOLID, зачем мне ещё GRASP?
Для меня ответ примерно такой:
SOLID помогает понять, каким должен быть хороший дизайн.
А GRASP помогает рассуждать о том, как к этому дизайну прийти.
Например, SOLID говорит:
Не создавай класс с большим количеством разных обязанностей.
А GRASP помогает задать вопрос:
А кому тогда передать эту ответственность?
SOLID говорит:
Не завязывайся на конкретную реализацию.
GRASP помогает спросить:
Какая часть системы здесь является вариативной и как её лучше изолировать?
SOLID говорит:
Используй полиморфизм там, где это уместно.
GRASP объясняет, какие дизайнерские проблемы можно решать с помощью полиморфизма.
Поэтому я бы не рассматривал GRASP и SOLID как конкурирующие подходы.
Скорее, они хорошо дополняют друг друга.
Главное, что я вынес из GRASP
Наверное, самая важная мысль для меня заключается в том, что хорошее проектирование — это прежде всего распределение ответственности.
Мы постоянно решаем:
Кто должен это делать?
Кто должен это создавать?
Кто должен об этом знать?
Кто должен зависеть от кого?
Что произойдёт, если эта часть изменится?
И GRASP предлагает для таких решений целый набор полезных способов мышления.
Причём один и тот же фрагмент кода вполне может одновременно демонстрировать несколько принципов.
Например:
OrderService
↓
PaymentGateway
↑
StripeGateway
Здесь можно увидеть и Polymorphism, и Indirection, и Protected Variations, и Low Coupling.
И это нормально.
Поэтому я бы не пытался определить:
«Вот этот класс — это Polymorphism, а вот тот — Protected Variations».
Лучше думать иначе:
Какую проблему я сейчас решаю и почему именно такое распределение ответственности делает систему лучше?
И вот в этом смысле GRASP для меня оказался гораздо интереснее, чем просто ещё один список принципов проектирования.

Add new comment