Принципы GRASP
4 июля 2022
Набор принципов GRASP, general responsibility assignment software principles, что переводится как "общие принципы распределения обязанностей", помогает, как следует из названия, правильно выбрать в какой объект или модуль распределить определённую обязанность. Под обязанностью здесь подразумевается знание/хранение информации и/или проведение каких-либо действий.
Принципы сформулированы в 1997 году Крэгом Ларманом в книге "Applying UML and Patterns" (на русском выходила под названием «Применение UML 2.0 и шаблонов проектирования»).
Всего их девять. Четыре основных и пять дополнительных.
Information expert / Информационный эксперт
Как разделить обязанности?
Обязанности назначаются модулю, который имеет информацию, необходимую для их выполнения.
Методы для выполнения действий стоит размещать ближе к информации, над которой эти действия проводятся.
final class ShoppingCart
{
/**
* @var Item[]
*/
private array $items;
public function __construct(Items $items)
{
$this->items = $items;
}
public function getTotalAmount()
{
return array_reduce($this->items, function ($sum, $item) {
return $sum + $item->getAmount();
}, 0);
}
}
Принцип позволяет добиваться большей инкапсуляции. То есть чтобы работающий с публичным API меньше встречался с ситуацией "все кишки наружу". Однако, он не всегда даёт хороший результат. Об этом ниже в "Low Coupling" и "High Cohesion".
Creator / Создатель
Кто должен отвечать за создание нового экземпляра класса?
Класс B должен создавать экземпляры класса A если выполняется одно из следующих условий в порядке важности: - B агрегирует A. - B содержит A. - B записывает экземпляры A. - B активно использует объекты A. - B обладает данными для инициализации экземпляров A.
final class ShoppingCart // B
{
/**
* @var Item[]
*/
private array $items = [];
public function addItem(string $name, int $amount)
{
$this->items[] = new Item($name, $amount); // A
}
}
Если операция создания сложная, что часто бывает, предпочтительнее использовать отдельную фабрику.
Low coupling / Низкая связанность
Как для зависимости снизить влияние изменений и повысить возможность повторного использования?
Держать степень связанности низкой.
Связанность — мера, определяющая насколько жёстко один элемент связан с другими элементами, либо каким количеством данных о других элементах он обладает:
final class A
{
private B $b; // <-- A связан с B
}
final class A
{
public function doit()
{
(new B())->doit(); // <-- A связан с B
}
}
final class A
{
public function doit(B $b) // <-- A связан с B
{
// ...
}
}
final class A extends B // <-- A связан с B
{
// ...
}
final class A implements B // <-- A связан с B
{
// ...
}
Низкая связанность означает малое количество таких связей и данных. Высокая — большое. При высокой связанности:
- Правки в одной части кода, как правило, затрагивают множество других частей кода.
- Тяжело понимать код. Приходится прыгать по разным классам.
- Как следствие, тяжело повторно использовать класс.
При применении принципа внимание стоит уделять нестабильным быстро меняющимся частям системы.
Не стоит доводить связанность до нуля, иначе мы получим набор DTO и несколько объектов, выполняющих над ними действия. То есть достаточно бедную, вырожденную объектную модель.
Также не представляет проблемы связанность со стабильными классами и функциями. Например, со стандартными функциями и классами самого языка или с любыми другими, которые широко используются и практически не меняются.
High cohesion / Высокая связность или высокое функциональное зацепление
Как обеспечить возможность управления сложностью?
Обязанности внутри элемента должны быть тесно связаны и сфокусированы.
Все ли данные и действия внутри элемента нужны для выполнения основной задачи? Нет ничего лишнего? Не осталось ли что-то вовне?
// Lowest cohesion
final class DataProvider
{
public function getItemsFromDatabase()
{
// ...
}
public function getItemsFromAPI()
{
// ...
}
}
Градации функционального зацепления таковы:
- Очень низкое - обязанности из не связанных областей (как выше).
- Низкое- обязанности из одной области, но их слишком много.
- Среднее - несложные обязанности из разных областей, логически связанных с концепцией класса, но не связанных между собой.
- Сильное - среднее количество обязанности из одной области, взаимодействует с другими классами из той же области
Низкое функциональное зацепление нормально в некоторых ситуациях:
- Когда это упрощает поддержку одним человеком.
- Когда это улучшает производительность в распределённых системах.
Controller / Контроллер
Кто должен отвечать за обработку внешних событий?
Класс, который подходит под одно из условий:
- Представляет всю систему в целом, устройство или подсистему.
- Представляет собой сценарий в рамках которого выполняется обработка.
Тут речь о, в общем-то, уже классическом понимании контроллера, как оно есть в популярных фреймворках. То есть прослойки между входными (интерфейсом) и, собственно, логикой предметной области. Важный момент в том, что контроллер не должен сам выполнять нужные действия, а должен лишь делегировать и координировать их.
final class PostController
{
public function actionCreate(
Request $request,
CreatePostOperation $operation,
CreatePostValidator $validator
)
{
$data = $request->getParsedBody;
$validator->validate($data);
return $operation->handle($data);
}
}
Такое разделение позволяет улучшить повторное использование и тестируемость компонентов, которым в контроллере мы делегируем выполнение.
Indirection / Посредник
Как распределить обязанности чтобы обеспечить отсутствие прямого связывания между компонентами? Как снизить уровень связывания?
Перенести взаимодействие между компонентами в отдельный промежуточный объект.
final class Item
{
public function save()
{
(new ItemStorage())->save($this);
}
}
final class ItemStorage // Посредник
{
public function save(Item $item)
{
(new DatabaseStorage())->save($item);
}
}
final class DatabaseStorage
{
public function save($object)
{
// Логика сохранения в базу
}
}
Polymorphism / Полиморфизм
Как обрабатывать альтернативные варианты поведения на основе типа? Как создавать подключаемые программные компоненты?
Если поведение объектов класса может измениться, распределяйте обязанности при помощи полиморфных операций.
Алгоритмы с if-then-else приходится модифицировать при изменении вариантов. Это затрудняет поддержку кода. Полиморфизм позволяет добавлять новые способы обработки без модификации основного кода:
interface ItemInterface
{
public function getType(): string;
}
final class MessageSender
{
public function handle(ItemInterface $item)
{
if ($item->getType() === 'email') {
$this->sendEmail($item);
} elseif ($item->getType() === 'sms') {
$this->sendSms($item);
}
// Сюда добавляем новые варианты
}
}
// ↓↓↓
interface ItemInterface
{
public function send(): void;
}
final class MessageSender
{
public function handle(ItemInterface $item)
$item->send();
}
}
Полиморфизм не стоит применять если вероятность вариативного поведения низкая или неизвестная.
Protected variations / Устойчивость к изменениям
Как спроектировать систему так, чтобы изменения в одном месте не оказывало влияния на изменения в другом месте?
Идентифицировать точки вариаций (неустойчивости, изменений). Распределить обязанности так, чтобы обеспечить устойчивый интерфейс (контракт). То есть скрыть детали реализации за интерфейсом.
Хороший пример — отсылка SMS. Так как горький опыт говорит нам что шлюзы отказывают, меняют цены и условия, стоит сразу ввести интерфейс:
interface SMSSenderInterface
{
public function send(string $phone, string $message);
}
final class NexmoSmsSender implements SMSSenderInterface
{
// ...
}
final class SmscSmsSender interface SMSSenderInterface
{
// ...
}
В остальном коде мы везде применяем исключительно SMSSenderInterface
, что при смене шлюза позволяет нам не менять ни строчки кода кроме конфигурации контейнера или инициализации где мы выбираем новую реализацию SMSSenderInterface
.
Не стоит применять принцип если вы не можете нормально идентифицировать точки изменений в системе.
Если система для вас новая, можно воспользоваться техническими средствами и посмотреть на git churn.
Pure fabrication / Чистая выдумка
Какой класс должен обеспечить high cohesion / low coupling и другие принципы если information expert не даёт ответ на этот вопрос?
Создать новый искусственный класс с высоким зацеплением и слабым связыванием не являющийся частью предметной области.
Как сохранять Item
в базу данных? Согласно Information Expert, делать это должен сам Item
, но:
- В
Item
окажется множество операций, связанных не с самой доменной моделью, а с записью в базу. То есть низкая степень зацепления. - Класс завязывается на интерфейсы для взаимодействие с базой. То есть возрастает степень связывания.
- Задача взаимодействия с базой достаточно общая, её придётся решать каждый раз копированием.
Information Expert тут явно не даёт результата, поэтому создаётся новый класс:
final class Item
{
// Чистая бизнес-логика
}
final class DatabaseStorage
{
public function save($object)
{
// Логика сохранения в базу
}
}
Классы можно выделять в соответствии с объектами предметной области и в соответствии с поведением. Здесь у нас второй случай
Не стоит злоупотреблять Pure fabrication. Применяйте только если не сработали другие принципы.
Комментарии RSS по email OK