Привет, хабровчане. На связи Владислав Родин. В настоящее время я преподаю на портале 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 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. Его нарушение легко как определить, так и устранить, увеличив простоту восприятия кода (принцип наименьшего удивления), добавив возможность переиспользования и уменьшив число связей между классами.

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