Шаблоны GRASP: Information Expert (информационный эксперт)

    Привет, хабровчане. На связи Владислав Родин. В настоящее время я преподаю на портале 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.

    Information Expert


    Формулировка


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

    Пример нарушения


    Несмотря на кажущуюся простоту и очевидность, я уверен, что в коде любого проекта можно найти множество нарушений данного принципа.

    Рассмотрим простейшую систему классов: Order (заказ), содержащий список OrderItem'ов (строчек заказа), элементы которого в свою очередь содержат Good (товар) и его количество, а товар может содержать, например, цену, название и т.д.:

    @Getter
    @AllArgsConstructor
    public class Order {
        private List<OrderItem> orderItems;
        private String destinationAddress;
    }
    
    @Getter
    @AllArgsConstructor
    public class OrderItem {
        private Good good;
        private int amount;
    }
    
    @Getter
    @AllArgsConstructor
    public class Good {
        private String name;
        private int price;
    }
    


    Перед нами стоит простая задача: посчитать сумму заказа. Если подойти к решению данной задачи не очень вдумчиво, то можно сходу написать что-нибудь такое в клиентском коде, работающем с объектами класса Order:

    public class Client {
        public void doSmth() {
            
        }
        
        private int getOrderPrice(Order order) {
            List<OrderItem> orderItems = order.getOrderItems();
            
            int result = 0;
            
            for (OrderItem orderItem : orderItems) {
                int amount = orderItem.getAmount();
                
                Good good = orderItem.getGood();
                int price = good.getPrice();
                
                result += price * amount;
            }
            
            return result;
        }
    }
    


    Давайте проанализируем такое решение.

    Во-первых, если у нас начнет добавляться бизнес-логика, связанная с расчетом цены, код метода Client::getOrderPrice будет не только неминуемо разрастаться, но и обрастать всевозможными if-ами (скидка пенсионерам, скидка по праздничным дням, скидка из-за покупки оптом), что в конце концов приведет к тому, что данный код будет невозможно ни читать, ни тем более менять.

    Во-вторых, если построить UML-диаграмму, то можно обнаружить, что имеет место зависимость класса Client аж на 3 класса: Order, OrderItem и Good. В него вытянута вся бизнес-логика по работе с этими классами. Это означает, что если мы захотим переиспользовать OrderItem или Good отдельно от Order (например, для подсчета цены товаров, оставшихся на складах), мы просто не сможем этого сделать, ведь бизнес-логика лежит в клиентском коде, что приведет к неминуемому дублированию кода.

    В данном примере, как и практически везде, где есть цепочка из get'ов, нарушен принцип Information Expert, ведь обрабатывает информацию клиентский код, а содержит ее Order.

    Пример применения


    Попробуем перераспределить обязанности согласно принципу:

    @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;
        }
    }
    
    @Getter
    @AllArgsConstructor
    public class OrderItem {
        private Good good;
        private int amount;
    
        public int getPrice() {
            return amount * good.getPrice();
        }
    }
    
    @Getter
    @AllArgsConstructor
    public class Good {
        private String name;
        private int price;
    }
    
    public class Client {
        public void doSmth() {
            Order order = new Order(new ArrayList<>(), "");
            order.getPrice();
        }
    }
    


    Теперь информация обрабатывается в содержащем ее классе, клиентский код зависит лишь на Order, ничего не подозревая об его внутреннем устройстве, а классы Order, OrderItem и Good, либо OrderItem и Good могут быть собраны в отдельную библиотеку, которую можно использовать в различных участках проекта.

    Вывод


    Information Expert, следующий из инкапсуляции, является одним из фундаментальнейших принципов разделения ответственности GRASP. Его нарушение легко как определить, так и устранить, увеличив простоту восприятия кода (принцип наименьшего удивления), добавив возможность переиспользования и уменьшив число связей между классами.

    Приглашаем на бесплатный вебинар в рамках которого можно будет изучить особенности монолитного приложения, многоуровневой и бессерверной архитектур. Подробно рассмотреть систему, управляемую событиями, сервис-ориентированную систему и микросервисную архитектуру.
    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

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

      0
      нового курса «Архитектура и шаблоны проектирования»


      Ссылка обещает новый курс, а приводит на регистрацию на непонятный вебинар.
      Так задумано?
        0
        Да. Вебинар абсолютно бесплатный и имеет непосредственное отношение к курсу. Посмотрев его можно в принципе оценить процесс изложения плюс, возможно, получить какие-то новые знания. Если же не хочется смотреть вебинар, а есть цель сразу зарегистрироваться на курс, можно закрыть крестиком этот попап и под ним будет кнопочка прохождения тестирования и регистрации
          0
          На момент написание коммента открывался не вебинар, а страница регистрации на него. И это не выглядело как попап, поэтому и спросил.
        +1
        Мне кажется, стоит добавить тот код, что будет учитывать «скидка пенсионерам, скидка по праздничным дням, скидка из-за покупки оптом» (хоть что-нибудь одно), иначе преимущество кода после рефакторинга не очевидно.
          0
          @Getter
          @AllArgsConstructor
          public class OrderItem {
              private Good good;
              private int amount;
          
              public int getPrice() {
                  return good.getPrice();
              }
          }
          

          rodinvv, здесь случайно не забыли учесть количество товара в элементе заказа?
            0
            Забыл, спасибо!
            0
            public int getPrice() {
            return good.getPrice();
            }


            Наверное, good.getPrice()*amount, не?

            Но вообще, боже мой, GRASP паттерны! Я их еще в 2001м году учил:) Хуже они не стали, конечно.
              0
              Да, конечно, спасибо!

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

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