Привет, Хабр. Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в 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 позволяет добиться минимального количества связей между классами и большей переиспользуемости.



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