Специально к старту нового набора на курс «Архитектура и шаблоны проектирования» я написал небольшой материал, которым с радостью делюсь с вами.
Введение
Описанные в книге Craig'а Larman'а «Applying UML and patterns, 3rd edition», GRASP'овские паттерны являются обобщением GoF'овских паттернов, а также непосредственным следствием принципов ООП. Они дополняют недостающую ступеньку в логической лестнице, которая позволяет получить GoF'овские паттерны из принципов ООП. Шаблоны GRASP являются скорее не паттернами проектирования (как GoF'овские), а фундаментальными принципами распределения ответственности между классами. Они, как показывает практика, не обладают особой популярностью, однако анализ спроектированных классов с использованием полного набора GRASP'овских паттернов является необходимым условием написания хорошего кода.
Полный список шаблонов GRASP состоит из 9 элементов:
- Information Expert
- Creator
- Controller
- Low Coupling
- High Cohesion
- Polymorphism
- Pure Fabrication
- Indirection
- Protected Variations
В прошлый раз мы обсудили принцип Information Expert. Сейчас я предлагаю рассмотреть похожий на него Creator.
Creator
Формулировка
Данный паттерн решает такую же типовую задачу как и его предшественник: создавать экземпляры класса должен класс, которому они нужны.
Пример нарушения
Рассмотрим все ту же задачу с заказами и товарами. Предположим, что написанный нами код соблюдает Information Expert:
@Setter
@Getter
@AllArgsConstructor
public class Order {
private List<OrderItem> orderItems;
private String destinationAddress;
public int getPrice() {
int result = 0;
for(OrderItem orderItem : orderItems) {
result += orderItem.getPrice();
}
return result;
}
}
@Setter
@Getter
@AllArgsConstructor
public class OrderItem {
private Good good;
private int amount;
public int getPrice() {
return amount * good.getPrice();
}
}
@Setter
@Getter
@AllArgsConstructor
public class Good {
private String name;
private int price;
}
Несмотря на опять-таки кажущуюся тривиальность изучаемого нами принцип, в каком-нибудь клиентском коде можно будет обнаружить такое:
public class Client {
public void doSmth() {
Good good = new Good("name", 2);
OrderItem orderItem = new OrderItem(good, amount);
List<OrderItem> orderItems = new ArrayList<>();
orderItems.add(orderItem);
Order order = new Order(orderItems, "abc");
// client code
}
}
Если построить UML-диаграмму классов, то можно обнаружить, что класс Client теперь зависит на класс Order и на все его внутренности: OrderItem и Good. Таким образом, мы не можем переиспользовать класс Client без указанных выше классов, которые Client'у и не нужны. Мы фактически свели на нет результат всех усилий по соблюдению Information Expert, потому как класс Client создавал все объекты.
В legacy — проектах часто можно увидеть как один класс создает объект другого и пробрасывает его в качестве параметра в методе через 5-6 классов, внутри которых этот объект не используется. Это есть ни что иное как добавление нескольких зависимостей на пустом месте.
Пример применения
Давайте поправим распределение ответственности между классами так, чтобы распределение удовлетворяло не только Information Expert, но и Creator:
@Setter
@Getter
public class Order {
private List<OrderItem> orderItems = new ArrayList<>();
private String destinationAddress;
public Order(String destinationAddress) {
this.destinationAddress = destinationAddress;
}
public int getPrice() {
int result = 0;
for(OrderItem orderItem : orderItems) {
result += orderItem.getPrice();
}
return result;
}
public void addOrderItem(int amount, String name, int price) {
orderItems.add(new OrderItem(amount, name, price));
}
}
@Setter
@Getter
public class OrderItem {
private Good good;
private int amount;
public OrderItem(int amount, String name, int price) {
this.amount = amount;
this.good = new Good(name, price);
}
public int getPrice() {
return amount * good.getPrice();
}
}
@Setter
@Getter
@AllArgsConstructor
public class Good {
private String name;
private int price;
}
Теперь число зависимостей между классами будет минимальным. Клиентский код несколько упрощается и может выглядеть следующим образом:
public class Client {
public void doSmth() {
Order order = new Order("address");
order.addOrderItem(amount, name, price);
// client code
}
}
Вывод
Creator, о котором разработчики часто забывают, может рассматриваться как частный случай Information Expert, ведь вызов конструктора это тоже самое, что и вызов метода. Его соблюдение вместе с Information Expert позволяет добиться минимального количества связей между классами и большей переиспользуемости.
Узнать о курсе подробнее.
dopusteam
Как будет выглядеть создание зависимости, которой нужны другие зависимости, которым нужны ещё зависимости и т.д.? Зависимость от реализации, а не от интерфейса — с этим проблем не возникает?
TheShock
Обычно это решается фабриками. Когда хотите создать класс — создаёте его через фабрику. Фабрика знает все зависимости и передает классу при создании, а тот класс, который уже знает, что делать с сущность при создании передаёт фабрике только аргументы, без зависимостей.
В итоге, у вас при инициализации приложения создаётся много фабрик, в них пробрасываются зависимости, а потом вы просто используете фабрики и ничего не думаете про проброску зависимостей.
dopusteam
Но суть паттерна — «создавать экземпляры класса должен класс, которому они нужны».
Создание через фабрику явно противоречит этому.
Ну и всё таки пример хотелось бы посмотреть
TheShock
Ну вы не совсем правильно воспринимаете фабрику. Она не отменяет "создавать экземпляры класса должен класс, которому они нужны". Она только меняет КАК создаются экземпляры класса. То есть в обоих примерах ниже они будут создаваться в Client, но в одном — напрямую, а во втором — через фабрику:
В результате код получается какой-то такой:
Потому обмазываете это DI Container'ом и полетели
dopusteam
Справедливости ради, 'Класс создаёт экземпляры класса' и 'Класс запрашивает у фабрики экземпляры класса' — это концептуально разные подходы.
Непонятно преимущество перед стандартным подходом с внедрением зависимостей через конструктор. В чём плюс подхода, когда через конструктор получаем фабрику, а в методах получаем из фабрики зависимости? Почему сразу не получить зависимости в конструкторе тогда?
TheShock
В том, что вам нужно пробрасывать все зависимости везде, где создается класс. А если меняется список зависимостей — вместо того, чтобы поменять его в одном месте рядом с самим классом — нужно бегать по всем местам и тоже как-то пробрасывать новые зависимости.
Класс говорит: "когда создать", а фабрика: "как создать".
dopusteam
Так это же не в моём случае, а в вашем. Если изменятся зависимости Order, вам придётся менять OrderFactory. А если там ещё какие то зависимости, которые тоже через фабрики получаются, то вообще непонятно сколько придётся менять кода. В случае инъекции в конструктор и с использованием DI контейнера какого нибудь, я вообще явно ничего не прокидываю, а запрашиваю, указав в конструкторе класса.
В случае использования инъекции в конструктор, я поменяю в одном месте — в конструкторе класса, где изменился список зависимостей. Остальное DI контейнер сделает.
Тогда суть паттерна не ясна мне. «создавать экземпляры класса должен класс, которому они нужны» — для чего эта формулировка, если по факту она очень сильно притянута? Класс всё ещё не создаёт ничего, он определяет КОГДА создавать в данном случае.
TheShock
Ну и как вы с DI Container'ом то же самое без фабрики сделаете?) Ну то есть, если вам необходимо, чтобы в классе через конструктор передавалось два уникальных параметра и две зависимости — как этого добъётесь? Покажите пример, как создали бы этот класс?
dopusteam
Я выше скидывал пример, там вместо использования фабрики, Client передаёт свои зависимости в Order.
Вот тут действительно класс сам создаёт нужную зависимость.
Это если оставить в стороне вопрос, что такая сущность выглядит странно немного. Ну т.е. в идеале если мы создаём сущность 'Заказ', лучше чтоб она не включала в себя зависимости в виде сервисов.
Вообще, описание паттерна не звучит как 'создавать экземпляры класса должен класс, которому они нужны'
TheShock
Ну так в таком случае ВСЕ, кто используют этот класс должны знать о его зависимостях. И чем больше использований — тем больше изменений при рефакторинге. Фабрика значительно лучше тем, что нужно изменять только в одном месте. Плюс ещё в том, что:
Фабрика даёт нам адекватный интерфейс без зависимостей.
Но вы в какой-то момент начали рассказывать про DI Container, который позволяет вообще зависимости не указывать и фабрики не использовать. И мне захотелось его увидеть.
dopusteam
Я говорил про DI container, использование которого позволяет перечислить зависимости в конструкторе класса и не создавать явно никакие сервисы.
В случае создания сущности типа 'Заказ', если ей нужны
какие то runtime параметры — да, это делается через фабрику. Но это частный случай.
Но изначальный вопрос был — 'Что делать, если там зависимость у которой другие зависимости и прочее?'. Если я руками создавать будут класс, которому нужно передать ещё n классов, которым ещё n классов — то что получится? Как будет код выглядеть? Они все сами создавать будут зависимости? Что в таком случае с зависимостью от реализации, а не от абстракции?
Для примера — создание контроллера, которому нужен сервис приложения, которому нужны какие нибудь репозиторий, сервис логирования, интеграции и т.д.
TheShock
Ну вот для вашего примера прекрасно подходят фабрики. Они создают всё, что нужно и сами думают о зависимостях.