Статьи

Проблема пиццы — строитель против декоратора

Постановка задачи

Нам нужно создать программное обеспечение для пиццерии, которая хочет готовить различные виды пиццы, например, «Куриная пицца», «Плоский хлеб», «Пепперони Пицца с добавлением сырного сыра», добавлять на нее начинки.

Попробуем выяснить, какой шаблон проектирования подходит для этой постановки задачи и по какому сценарию. Традиционно, для проблемы с пиццей, чаще всего используется шаблон строителя. Тем не менее, есть некоторые примеры использования декоратора, оба подхода верны, но есть разница в сценарии использования Builder — это шаблон создания объекта, тогда как decorator используется для изменения уже построенного объекта во время выполнения.
Давайте попробуем понять это на примерах:

  1. Образец Строителя:

    Здесь используется тот случай, когда пицца готовится за один раз с заданными характеристиками.

    Давайте посмотрим класс пиццы:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public class Pizza {
     
        private float totalPrice = 0;
     
        private Size size;
        private Topping topping;
        private Crust crust;
        private Cheese cheese;
     
        public Size getSize() {
            return size;
        }
     
        public void setSize(Size size) {
            this.size = size;
        }
     
        public Topping getTopping() {
            return topping;
        }
     
        public void setTopping(Topping topping) {
            this.topping = topping;
        }
     
        public Crust getCrust() {
            return crust;
        }
     
        public void setCrust(Crust crust) {
            this.crust = crust;
        }
     
        public Cheese getCheese() {
            return cheese;
        }
     
        public void setCheese(Cheese cheese) {
            this.cheese = cheese;
        }
     
        public float getTotalPrice() {
            return totalPrice;
        }
     
        public void addToPrice(float price) {
            this.totalPrice = totalPrice + price;
        }
    }

    4 класса enum:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public enum Cheese {
        AMERICAN {
            public float getCost() {
                return 40;
            }
        }, ITALIAN {
            public float getCost() {
                return 60;
            }
        };
     
        public abstract float getCost();
     
    }
     
    public enum Crust {
     
          THIN  {
            public float getCost(){
                return 70;
            }
        } , STUFFED{
            public float getCost(){
                return 90;
            }
        };
     
        public abstract float getCost();
    }
     
    public enum Size {
        MEDIUM {
            public float getCost() {
                return 100;
            }
        }, LARGE {
            public float getCost() {
                return 160;
            }
        };
     
        public abstract float getCost();
    }
     
    public enum Topping {
     
        PEPPERONI {
            public float getCost(){
                return 30;
            }
        }, CHICKEN{
            public float getCost(){
                return 35;
            }
        }, MUSHROOM{
            public float getCost(){
                return 20;
            }
        };
     
        public abstract float getCost();
    }

    Класс PizzaBuilder:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class PizzaBuilder {
     
        Pizza pizza = new Pizza();
     
        public PizzaBuilder withTopping(Topping topping) {
            pizza.setTopping(topping);
            pizza.addToPrice(topping.getCost());
            return this;
        }
     
        public PizzaBuilder withSize(Size size) {
            pizza.setSize(size);
            pizza.addToPrice(size.getCost());
            return this;
        }
     
        public PizzaBuilder withCrust(Crust crust) {
            pizza.setCrust(crust);
            pizza.addToPrice(crust.getCost());
            return this;
        }
     
        public Pizza build() {
            return pizza;
        }
     
        public double calculatePrice() {
            return pizza.getTotalPrice();
        }
     
    }

    Тестовый пример:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    public class PizzaBuilderTest {
     
        @Test
        public void shouldBuildThinCrustChickenPizza(){
            Pizza pizza = new PizzaBuilder().withCrust(Crust.THIN).withTopping(Topping.CHICKEN).withSize(Size.LARGE).build();
            assertEquals(Topping.CHICKEN,pizza.getTopping());
            assertEquals(Size.LARGE,pizza.getSize());
            assertEquals(Crust.THIN,pizza.getCrust());
            assertEquals(265.0,pizza.getTotalPrice(),0);
        }
    }
  2. Образец Декоратора:

    Шаблон «Декоратор» используется для динамического добавления или удаления дополнительных функций или обязанностей из объекта без воздействия на исходный объект. Вариант использования: сначала готовится базовая пицца, а затем к ней добавляются различные характеристики.

    Здесь нам нужен интерфейс (Pizza) для BasicPizza (Конкретный компонент), который мы хотим декорировать, и класс PizzaDecorator, который содержит поле ссылки интерфейса Pizza (оформленный).

    Интерфейс пиццы:

    1
    2
    3
    4
    public interface Pizza {
        public String bakePizza();
        public float getCost();
    }

    Реализация базовой пиццы:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class BasePizza implements Pizza{
     
        public String bakePizza() {
            return "Basic Pizza";
        }
        public float getCost(){
            return 100;
        }
    }

    PizzaDecorator класс:

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    public class PizzaDecorator implements Pizza {
        Pizza pizza;
        public PizzaDecorator(Pizza newPizza) {
            this.pizza = newPizza;
        }
     
        public String bakePizza() {
            return pizza.bakePizza();
        }
     
        @Override
        public float getCost() {
            return pizza.getCost();
        }
    }

    2 декоратора: гриб и пепперони

    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    public class Mushroom extends PizzaDecorator {
     
        public Mushroom(Pizza newPizza) {
            super(newPizza);
        }
     
        @Override
        public String bakePizza() {
            return super.bakePizza() + " with Mashroom Topings";
        }
     
        @Override
        public float getCost() {
            return super.getCost()+80;
        }
    }
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    public class Pepperoni extends PizzaDecorator {
     
        public Pepperoni(Pizza newPizza) {
            super(newPizza);
        }
     
        @Override
        public String bakePizza() {
            return super.bakePizza() + " with Pepperoni Toppings";
        }
     
        @Override
        public float getCost() {
            return super.getCost()+110;
        }
    }

    Тестовый кейс:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class PizzaDecoratorTest {
     
        @Test
        public void shouldMakePepperoniPizza(){
            Pizza pizza = new Pepperoni(new BasePizza());
            assertEquals("Basic Pizza with Pepperoni Toppings",pizza.bakePizza());
            assertEquals(210.0,pizza.getCost(),0);
        }
    }

Разница

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

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

Таким образом, следует использовать строитель, если он хочет ограничить создание объекта определенными свойствами / возможностями. Например, есть 4-5 атрибутов, которые необходимо установить до создания объекта, или мы хотим заморозить создание объекта, пока определенные атрибуты еще не установлены. По сути, используйте его вместо конструктора — как утверждает Джошуа Блох в Effective Java, 2nd Edition . Строитель предоставляет атрибуты, которые должен иметь сгенерированный объект, но скрывает, как их устанавливать.

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

Другим подходом может быть использование Factory Pattern; если мы не хотим раскрывать атрибуты и хотим, чтобы создание «пиццы» было «волшебным» внутри, тогда на основе некоторых атрибутов. Мы рассмотрим эту реализацию с использованием Factory Pattern в следующем посте.

Ссылка: Проблема пиццы — строитель против декоратора от нашего партнера JCG Анируд Бхатнагар в блоге Анирудх Бхатнагар.