Шаблоны GRASP: Creator (создатель)

    Привет, Хабр. Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

    Специально к старту нового набора на курс «Архитектура и шаблоны проектирования» я написал небольшой материал, которым с радостью делюсь с вами.





    Введение


    Описанные в книге 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 позволяет добиться минимального количества связей между классами и большей переиспользуемости.



    Узнать о курсе подробнее.


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Комментарии 15

      0

      Как будет выглядеть создание зависимости, которой нужны другие зависимости, которым нужны ещё зависимости и т.д.? Зависимость от реализации, а не от интерфейса — с этим проблем не возникает?

        0

        Обычно это решается фабриками. Когда хотите создать класс — создаёте его через фабрику. Фабрика знает все зависимости и передает классу при создании, а тот класс, который уже знает, что делать с сущность при создании передаёт фабрике только аргументы, без зависимостей.


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

          0
          Но суть паттерна — «создавать экземпляры класса должен класс, которому они нужны».
          Создание через фабрику явно противоречит этому.
          Ну и всё таки пример хотелось бы посмотреть
            0

            Ну вы не совсем правильно воспринимаете фабрику. Она не отменяет "создавать экземпляры класса должен класс, которому они нужны". Она только меняет КАК создаются экземпляры класса. То есть в обоих примерах ниже они будут создаваться в Client, но в одном — напрямую, а во втором — через фабрику:


            public class Client {
                public void doSmth() {
                    Order order = new Order("address");
                }
            }
            
            public class Client {
                public void doSmth() {
                    Order order = orderFactory.create("address");
                }
            }

            В результате код получается какой-то такой:


            class Order {
                constructor (
                    // сообственные свойства
                    public readonly OrderName name,
                    public readonly Money cost,
            
                    // зависимости
                    private readonly Factory1 f1,
                    private readonly SomeUniqueClass uniq
                ) {}
            }
            
            class OrderFactory {
                constructor (
                    private readonly Factory1 f1,
                    private readonly SomeUniqueClass uniq
                ) {}
            
                Order Create(OrderName name, Money cost) {
                    return new Order(name, cost, f1, uniq);
                }
            }
            
            class Client {
                constructor (
                    // зависимости
                    private readonly OrderFactory orderFactory,
                ) {}
            
                public void Start() {
                    Order order = orderFactory.Create(
                        new OrderName("address"),
                        new Money(100)
                    );
                }
            }
            
            class Installer {
                static void main => new Installer().install();
            
                void install () {
                    var f1 = new Factory1();
                    var f2 = new Factory2();
            
                    var uniq = new SomeUniqueClass(f2);
            
                    var orderFactory = new OrderFactory(f1, uniq);
            
                    var client = new Client(orderFactory);
                    client.Start();
                }
            }

            Потому обмазываете это DI Container'ом и полетели

              0
              Справедливости ради, 'Класс создаёт экземпляры класса' и 'Класс запрашивает у фабрики экземпляры класса' — это концептуально разные подходы.

              Непонятно преимущество перед стандартным подходом с внедрением зависимостей через конструктор. В чём плюс подхода, когда через конструктор получаем фабрику, а в методах получаем из фабрики зависимости? Почему сразу не получить зависимости в конструкторе тогда?

              class Order {
                  constructor (
                      // сообственные свойства
                      public readonly OrderName name,
                      public readonly Money cost,
              
                      // зависимости
                      private readonly Factory1 f1,
                      private readonly SomeUniqueClass uniq
                  ) {}
              }
              
              class Client {
                  constructor (
                      private readonly Factory1 f1,
                      private readonly SomeUniqueClass uniq
                  ) {}
              
                  public void Start() {
                      Order order = new Order(
                          new OrderName("address"),
                          new Money(100),
                          f1,
                          uniq
                      );
                  }
              }
              
              class Installer {
                  static void main => new Installer().install();
              
                  void install () {
                      var f1 = new Factory1();
                      var f2 = new Factory2();
              
                      var uniq = new SomeUniqueClass(f2);
              
                      var client = new Client(f1, uniq);
                      client.Start();
                  }
              }
                0
                В чём плюс подхода, когда через конструктор получаем фабрику, а в методах получаем из фабрики зависимости? Почему сразу не получить зависимости в конструкторе тогда?

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


                Справедливости ради, 'Класс создаёт экземпляры класса' и 'Класс запрашивает у фабрики экземпляры класса' — это концептуально разные подходы.

                Класс говорит: "когда создать", а фабрика: "как создать".

                  0
                  В том, что вам нужно пробрасывать все зависимости везде, где создается класс


                  Так это же не в моём случае, а в вашем. Если изменятся зависимости Order, вам придётся менять OrderFactory. А если там ещё какие то зависимости, которые тоже через фабрики получаются, то вообще непонятно сколько придётся менять кода. В случае инъекции в конструктор и с использованием DI контейнера какого нибудь, я вообще явно ничего не прокидываю, а запрашиваю, указав в конструкторе класса.

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


                  В случае использования инъекции в конструктор, я поменяю в одном месте — в конструкторе класса, где изменился список зависимостей. Остальное DI контейнер сделает.

                  Класс говорит: «когда создать», а фабрика: «как создать».
                  Тогда суть паттерна не ясна мне. «создавать экземпляры класса должен класс, которому они нужны» — для чего эта формулировка, если по факту она очень сильно притянута? Класс всё ещё не создаёт ничего, он определяет КОГДА создавать в данном случае.
                    0
                    В случае инъекции в конструктор и с использованием DI контейнера

                    Ну и как вы с DI Container'ом то же самое без фабрики сделаете?) Ну то есть, если вам необходимо, чтобы в классе через конструктор передавалось два уникальных параметра и две зависимости — как этого добъётесь? Покажите пример, как создали бы этот класс?


                    class Order {
                        constructor (
                            // сообственные свойства
                            public readonly OrderName name,
                            public readonly Money cost,
                    
                            // зависимости
                            private readonly Factory1 f1,
                            private readonly SomeUniqueClass uniq
                        ) {}
                    }
                      0
                      Я выше скидывал пример, там вместо использования фабрики, Client передаёт свои зависимости в Order.

                      class Client {
                          constructor (
                              private readonly Factory1 f1,
                              private readonly SomeUniqueClass uniq
                          ) {}
                      
                          public void Start() {
                              Order order = new Order(
                                  new OrderName("address"),
                                  new Money(100),
                                  f1,
                                  uniq
                              );
                          }
                      }


                      Вот тут действительно класс сам создаёт нужную зависимость.

                      Это если оставить в стороне вопрос, что такая сущность выглядит странно немного. Ну т.е. в идеале если мы создаём сущность 'Заказ', лучше чтоб она не включала в себя зависимости в виде сервисов.

                      Вообще, описание паттерна не звучит как 'создавать экземпляры класса должен класс, которому они нужны'
                        0
                        Я выше скидывал пример, там вместо использования фабрики, Client передаёт свои зависимости в Order.

                        Ну так в таком случае ВСЕ, кто используют этот класс должны знать о его зависимостях. И чем больше использований — тем больше изменений при рефакторинге. Фабрика значительно лучше тем, что нужно изменять только в одном месте. Плюс ещё в том, что:


                        Ну т.е. в идеале если мы создаём сущность 'Заказ', лучше чтоб она не включала в себя зависимости в виде сервисов.

                        Фабрика даёт нам адекватный интерфейс без зависимостей.


                        Но вы в какой-то момент начали рассказывать про DI Container, который позволяет вообще зависимости не указывать и фабрики не использовать. И мне захотелось его увидеть.

                          0
                          Но вы в какой-то момент начали рассказывать про DI Container, который позволяет вообще зависимости не указывать и фабрики не использовать. И мне захотелось его увидеть.


                          Я говорил про DI container, использование которого позволяет перечислить зависимости в конструкторе класса и не создавать явно никакие сервисы.
                          В случае создания сущности типа 'Заказ', если ей нужны
                          какие то runtime параметры — да, это делается через фабрику. Но это частный случай.
                          Но изначальный вопрос был — 'Что делать, если там зависимость у которой другие зависимости и прочее?'. Если я руками создавать будут класс, которому нужно передать ещё n классов, которым ещё n классов — то что получится? Как будет код выглядеть? Они все сами создавать будут зависимости? Что в таком случае с зависимостью от реализации, а не от абстракции?
                          Для примера — создание контроллера, которому нужен сервис приложения, которому нужны какие нибудь репозиторий, сервис логирования, интеграции и т.д.
                            0

                            Ну вот для вашего примера прекрасно подходят фабрики. Они создают всё, что нужно и сами думают о зависимостях.

        +1

        Как-то даже хуже стало. Теперь давайте представим, что мы не хотим динамики и всяких стрингов, а хотим статики. Вместо Good("pants") и Good("hats") мы вводим PantsGood и HatsGood. Всё, ваша идея сломалась.


        Я, конечно, понимаю, что в примере с товарами не всегда можно ввести статику (но иногда можно), но у вас в принципе нельзя ввести статику. А клиенту всё-равно нужно знать название товара.


        Или другой пример. Мы вводим такую сущность как GoodName, чтобы не использовать string (мало ли у нас строк в апке?) и Money, чтобы не использовать инт (чисел ещё больше разных).


        public class Good {
            private GoodName name;
            private Money price;
        }

        Ваш Client все-равно должен знать о куче классов


        public class Client {
            public void doSmth() {
                Order order = new Order("address");
                order.addOrderItem(amount, new GoodName("hats"), new Money(1000));
                // client code 
            }
        }
          0
          Мне не очень понятно, а если у меня не 3 свойства у OrderItem, а 10? Да и еще они меняются в процессе разработки. Поддерживать не очень-то просто.
          Получается, что я должен в 1 метод передавать 10 аргументов и не перепутать их порядок.

          Из приятного будет только то, что в классе клиент возможно не будет импорта класса OrderItem. Но только возможно, потому что вы не можете гарантировать, что бизнес логика не потребует оперировать в классе клиент OrderItem'ом без заказа (клиент делает запрос в поддержку об OrderItem).

          Не могли бы вы указать какие еще преимущества вашего подхода?
            0
            Присоединяюсь к комментарию. Данный подход к проектированию достаточно популярен, однако он больше нацелен на исключительно поведенческий вектор наращивания функционала. На практике же, в проектах если не преобладает, то по крайней мере имеет место быть структурный вектор наращивания функционала. То есть очень часто меняется набор полей у классов. Сам нахожусь в поиске элегантных архитектурных решений для реализации простоты структурных изменений.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое