General Responsibility Assignment Software Patterns (GRASP) - это можно перевести так - паттерны распределения ответственности. Суть в том, что это не строгие паттерны как у GoF (Gang of Four), это скорее тот смысл, которым мы наделяем объекты. Так что принципы распределения общей ответственности подходят больше чем паттерны. И поэтому название статьи не шаблоны GRASP как было бы идеологически верно, а все-таки принципы GRASP. Суммируя, GRASP - это хорошо документированные, стандартизированные и проверенные временем принципы объектно-ориентированного анализа, а не попытка привнести в ООП что-то принципиально новое.
GRASP выделяет следующие 9 принципов-шаблонов:
Теперь давайте рассмотрим каждый из них по порядку.
Information Expert
Information Expert - это скорее ответственность. Экспертом может быть любой класс. Тут даже дело не в проектировании, а в осведомленности. Зачем нам нужен информационный эксперт? Затем, что если объект владеет всей нужной информацией полностью для какой-то операции или функционала, то значит, что этот объект будет выполнять всё сам, либо получать делегировать выполнение этой операции от других.
Итак, рассмотрим пример. Есть некая система продаж. И есть класс Sale (продажа). Нам необходимо посчитать общую сумму продаж. Тогда кто будет считать общую сумму по продажам? Конечно же, класс - Sales, потому что именно он обладает всей информацией необходимой для этого.
Creator
Creator - суть ответственности такого объекта в том, что он создает другие объекты. Сразу напрашивается аналогия с фабриками. Так оно и есть. Фабрики тоже имеют именно ответственность - Создатель.
Но есть ряд моментов, которые должны выполняться, когда мы наделяем объект ответственностью создатель:
Controller
Уже где-то слышали, не правда ли? Controller - это объект-прослойка между UI-логикой и предметной (бизнес) логикой приложения. Создаем контроллер так, чтобы все вызовы от UI перенаправлялись именно ему и соответственно, все данные UI тоже получает только через него.
Напоминает известные MVC и MVP? Так и есть. Это по сути Presenter из MVP и контроллер из MVC. Разница между MVC и MVP есть, но это касается только направлений вызовов, ну и это тема естественно отдельной большой беседы.
Контроллер отвечает на такой вопрос: "Как UI должен взаимодействовать с доменной логикой приложения?"
Или ещё проще: "Как взаимодействовать с системой?". Это чем-то напоминает фасад дома. Фасад тоже предоставляет облегченный доступ к целой подсистеме объектов. Так и тут: контроллер для UI своего рода фасад, который предоставляет доступ к целой подсистеме бизнес-логики.
Low Coupling
Тоже известная штука. Если объекты в приложении сильно связанны, то любое их изменение приводит к изменениям во всех связанных объектах. А это неудобно и порождает баги. Вот поэтому везде пишут, что необходимо, чтобы код был слабо связан и зависел только от абстракций.
Например, если наш класс Sale реализует интерфейс ISale и другие объекты зависят именно от ISale, т.е. от абстракции, то когда мы захотим внести изменения касательно Sale - нам нужно будет всего лишь подменить реализацию.
Low Coupling встречается и в SOLID-принципах в виде Dependency Injection. Да, сейчас чаще можно услышать именно про этот принцип. Но суть здесь остается прежней:
"Программируйте на основе абстракций (интерфейс, абстрактный класс и т.п.), а не реализаций."
High Cohesion
High Cohesion - этот принцип тесно соотносится с слабой связанностью, и они идут в паре, когда одно всегда приводит к другому. Это как инь и янь, - оба всегда вместе. Дело в том, что наши классы, когда мы их задумываем, имеют какую-то одну ответственность (Single Resposibility Principle), например класс Sale (продажа) обладает всеми ответственностями которые касаются продаж, например, как мы уже говорили выше - вычисление общей суммы - Total .
Но давайте представим, что мы совершили оплошность и привнесли в Sale еще такую ответственность, как Payment (платеж). Что получится? Получится, что одни члены класса, которые касаются Sale, будут между собой достаточно тесно связанны, и также члены класса которые оперируют с Payment между собой - будут также тесно связаны, но в целом сцепленность класса SaleAndPayment будет низкой, так как, по сути, мы имеем дело с двумя обособленными частями в одном целом. И резонно будет провести рефакторинг и разделить класс SaleAndPayment на Sale и Payment, которые внутри будут тесно связанны или по другому сцеплены.
Так что высокая сцепленность - это как мера того, что мы не нарушаем Single Resposibility Principle. Вернее сказать, высокая сцепленность получается в результате соблюдения такого приципа из SOLID, как Single Resposibility Principle (SRP). Основной вопрос на который дает ответ высокая сцепленность - "Как поддерживать объекты сфокусированными на одной ответственности, понятными, управляемыми и как побочный эффект иметь слабо связанный код?"
Pure Fabrication
Здесь суть в выдуманном объекте. Вот такой себе принцип-хак! Но без него никак. Аналогом может быть шаблон Service (сервис) в парадигме DDD.
Иногда, сталкиваемся с таким вопросом: "Какой объект наделить ответственностью, но принципы информационный эксперт, высокая сцепленность не выполняются или не подходят?". Тогда нужно использовать синтетический класс который обеспечивает высокую сцепленность. Тут без примера точно не разобраться.
Итак - ситуация. Какой класс должен сохранять наш объект Sale в базу данных? Если подчиняется принципу "информационный эксперт", то Sale, но наделив его такой ответственностью мы получаем слабую сцепленность внутри него. Тогда можно найти выход, создав синтетическую сущность - SaleDao или SaleRepository, которая будет сильно сцеплена внутри и будет иметь единую ответственность - и правильно сохранять Sale в базу.
Так как мы выдумали этот объект, а не спроектировали с предметной области, то и он подчиняется принципу Pure Fabrication.
Indirection
Можно столкнуться с таким вопросом: "Как определить ответственность объекта и избежать сильной связанности между объектами, даже если один класс нуждается в функционале (сервисах), который предоставляет другой класс?" Необходимо наделить ответственностью объект посредник.
Например, возвратимся опять же к MVC. UI-логике на самом деле нужен не контроллер, а модель, доменная логика. Но мы не хотим, чтобы UI-логика была сильно связанна с моделью, и возможно в UI мы хотим получать данные и работать с разной предметной логикой. А связывать UI-слой с бизнес логикой было бы глупо, потому что получим код который будет сложный для изменений и поддержки. Выход - вводим контроллер как посредник между View и Model.
Protected Variations
Как спроектировать объекты, чтобы изменения в объекте или объекта не затрагивали других? Как избежать ситуации, когда меняя код одного объекта придется вносить изменения в множество других объектов системы? Кажется мы такое обсуждали уже. И пришли к выводу, что нужно использовать Low Coupling или Dependency Injection. Именно! Но суть текущего принципа немного в другом. Суть в том, чтобы определить "точки изменений" и зафиксировать их в абстракции (интерфейсе). "Точки изменений" - не что иное, как наши объекты, которые могут меняться.
То есть суть этого принципа в том, чтобы определить места в системе, где поведение может изменится и выделить абстракцию, на основе которой и будет происходить дальнейшее программирование с использованием этого объекта
Все это делается лишь для того, чтобы обеспечить устойчивость интерфейса. Если будет много изменений связанных с объектами, он, в таком случае считается неустойчивым, и тогда нужно выносить его в абстракцию, от которой он и будет зависеть, либо же распределять обязанности и ответственность в коде иным образом.
Polymorphism
Тоже знакомо, не так ли? Так вот это о том же полиморфизме, который мы знаем из ООП. Если заметить, то достаточно много паттернов GoF, да и вообще паттернов, построено на полиморфизме. Что он дает? Он дает возможность трактовать однообразно разные объекты с одинаковым интерфейсом (спецификацией). Давайте вспомним такие паттерны как Strategy, Chain of Resposibility, Command... - их много ещё всяких. И все по своей сути основываются на полиморфизме.
Полиморфизм решает проблему обработки альтернативных вариантов поведения на основе типа. Яркий пример этого шаблон из GoF - Strategy (Стратегия). Например, для реализации гибкого функционала для шифрования можно определить интерфейс IEncryptionAlgorithm с методом Encrypt, и объект создатель, который вернет IEncryptionAlgorithm, создав внутри себя актуальную реализацию этого интерфейса.
Явно или неясно, пользуясь другими принципами или шаблонами - мы тоже часто используем принципы или шаблоны GRASP при разработке и проектировании. Многие принципы пересекаются, просто возможно разные авторы по-разному смещают акценты, но на удивление все принципы и шаблоны верны - и SOLID, и GRASP и классические GoF-паттерны - и многие другие. И все их следует использовать с умом и балансировать ними, применяя совместно с какими-то хаками под свою систему, что позволяет разрабатывать действительно красивые, устойчивые и гибкие архитектуры и приложения.
Принципы GRASP хоть и упоминаются реже других, все же занимают свое место и играют важную роль именно в распределении обязанностей между объектами и заставляют нас думать еще и в таком ключе, а не просто подгонять шаблоны как кирпичи разной формы, чтобы в итоге выстроить пазл архитектуры.
В заключение - принципы GRASP не создают четкую структуру, которой мы должны следовать в коде. Это больше глубоко продуманное распределение ролей и ответственностей между всеми объектами, а также свойства, которыми эти объекты должны обладать для того, чтобы гармонично исполнять свои роли.