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

Приглашаем также всех желающих на открытый демо-урок
«Шаблоны GRASP». На этом занятии мы проанализируем функциональное разделение функционала и рассмотрим 9 шаблонов GRASP. Присоединяйтесь!


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

Паттерн проектирования Builder 

  • Паттерн проектирования Builder разработан для обеспечения гибкого решения различных задач создания объектов в объектно-ориентированном программировании.

  • Паттерн проектирования Builder позволяет отделить построение сложного объекта от его представления.

  • Паттерн Builder создает сложные объекты, используя простые объекты и поэтапный подход.

  • Паттерн предоставляет один из лучших способов создания сложных объектов.

  • Этот паттерн полезен для создания разных иммутабельных объектов с помощью одного и того же процесса построения объекта.

Паттерн Builder — это паттерн проектирования, который позволяет поэтапно создавать сложные объекты с помощью четко определенной последовательности действий. Строительство контролируется объектом-распорядителем (director), которому нужно знать только тип создаваемого объекта.

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

  • Product (продукт) - Класс, который определяет сложный объект, который мы пытаемся шаг за шагом сконструировать, используя простые объекты.

  • Builder (строитель) - абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Как правило, здесь объявляются (абстрактно) все этапы (buildPart), а их реализация относится к классам конкретных строителей (ConcreteBuilder).

  • ConcreteBuilder (конкретный строитель) - класс-строитель, который предоставляет фактический код для создания объекта-продукта. У нас может быть несколько разных ConcreteBuilder-классов, каждый из которых реализует различную разновидность или способ создания объекта-продукта.

  • Director (распорядитель) - супервизионный класс, под конролем котрого строитель выполняет скоординированные этапы для создания объекта-продукта. Распорядитель обычно получает на вход строителя с этапами на выполнение в четком порядке для построения объекта-продукта.

Паттерн проектирования Builder решает такие проблемы, как:

  • Как класс (тот же самый процесс строительства) может создавать различные представления сложного объекта?

  • Как можно упростить класс, занимающийся созданием сложного объекта?

Давайте реализуем пример со сборкой автомобилей, используя паттерн проектирования Builder.

Пример со сборкой автомобилей с использованием паттерна проектирования Builder

Шаг 1: Создайте класс Car (автомобиль), который в нашем примере является продуктом:

package org.trishinfotech.builder;

public class Car {

    private String chassis;
    private String body;
    private String paint;
    private String interior;
    
    public Car() {

        super();
    }

    public Car(String chassis, String body, String paint, String interior) {
        this();
        this.chassis = chassis;
        this.body = body;
        this.paint = paint;
        this.interior = interior;
    }

    public String getChassis() {
        return chassis;
    }

		public void setChassis(String chassis) {
        this.chassis = chassis;

    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public String getPaint() {
        return paint;
    }

    public void setPaint(String paint) {
        this.paint = paint;
    }
		public String getInterior() {
        return interior;
    }

    public void setInterior(String interior) {
        this.interior = interior;
    }

    public boolean doQualityCheck() {
        return (chassis != null && !chassis.trim().isEmpty()) && (body != null && !body.trim().isEmpty())
                && (paint != null && !paint.trim().isEmpty()) && (interior != null && !interior.trim().isEmpty());
    }

    @Override
    public String toString() {
        // StringBuilder class also uses Builder Design Pattern with implementation of java.lang.Appendable interface
        StringBuilder builder = new StringBuilder();
        builder.append("Car [chassis=").append(chassis).append(", body=").append(body).append(", paint=").append(paint)
        return builder.toString();
    }

}

Обратите внимание, что я добавил в класс проверочный метод doQualityCheck. Я считаю, что Builder не должен создавать неполные или невалидные Product-объекты. Таким образом, этот метод поможет нам в проверке сборки автомобилей.

Шаг 2: Создайте абстрактный класс/интерфейс CarBuilder, в котором определите все необходимые шаги для создания автомобиля.

package org.trishinfotech.builder;

public interface CarBuilder {

    // Этап 1
    public CarBuilder fixChassis();

    // Этап 2
    public CarBuilder fixBody();

    // Этап 3
    public CarBuilder paint();

    // Этап 4

    public CarBuilder fixInterior();

    // Выпуск автомобиля

    public Car build();
}

Обратите внимание, что я сделал тип CarBuilder типом возврата всех этапов, созданных здесь. Это позволит нам вызывать этапы по цепочке. Здесь есть один очень важный метод build, который заключается в том, чтобы получить результат или создать конечный объект Car. Этот метод фактически проверяет годность автомобиля и выпускает (возвращает) его только в том случае, если его сборка завершена успешно (все валидно).

Шаг 3: Теперь пора написать ConcreteBuilder. Как я уже упоминал, у нас могут быть разные варианты ConcreteBuilder, и каждый из них выполняет сборку по-своему, чтобы предоставить нам различные представления сложного объекта Car.

Итак, ниже приведен код ClassicCarBuilder, который собирает старые модели автомобилей.

package org.trishinfotech.builder;

public class ClassicCarBuilder implements CarBuilder {

    private String chassis;
    private String body;
    private String paint;
    private String interior;

    public ClassicCarBuilder() {
        super();
    }

    @Override
    public CarBuilder fixChassis() {
        System.out.println("Assembling chassis of the classical model");
        this.chassis = "Classic Chassis";
        return this;
    }

    @Override
    public CarBuilder fixBody() {
        System.out.println("Assembling body of the classical model");
        this.body = "Classic Body";
        return this;
    }

    @Override
    public CarBuilder paint() {
        System.out.println("Painting body of the classical model");
        this.paint = "Classic White Paint";
        return this;
    }
  
    @Override
    public CarBuilder fixInterior() {
        System.out.println("Setting up interior of the classical model");
        this.interior = "Classic interior";
        return this;
    }

    @Override
		public Car build() {
        Car car = new Car(chassis, body, paint, interior);
        if (car.doQualityCheck()) {
            return car;
        } else {
            System.out.println("Car assembly is incomplete. Can't deliver!");
        }
        return null;
    }

}

Теперь напишем еще один строитель ModernCarBuilder для сборки последней модели автомобиля.

package org.trishinfotech.builder;

public class ModernCarBuilder implements CarBuilder {

    private String chassis;
    private String body;
    private String paint;
    private String interior;

    public ModernCarBuilder() {
        super();
    }

    @Override
    public CarBuilder fixChassis() {
        System.out.println("Assembling chassis of the modern model");
        this.chassis = "Modern Chassis";
        return this;
    }

    @Override
    public CarBuilder fixBody() {
        System.out.println("Assembling body of the modern model");
        this.body = "Modern Body";
        return this;
    }
  
    @Override
    public CarBuilder paint() {
        System.out.println("Painting body of the modern model");
        this.paint = "Modern Black Paint";
        return this;
    }

    @Override
    public CarBuilder fixInterior() {
        System.out.println("Setting up interior of the modern model");
        this.interior = "Modern interior";
        return this;
    }

    @Override
    public Car build() {
        Car car = new Car(chassis, body, paint, interior);
        if (car.doQualityCheck()) {
            return car;
        } else {
            System.out.println("Car assembly is incomplete. Can't deliver!");
        }
        return null;
    }
}

 И еще один SportsCarBuilder для создания спортивного автомобиля.

package org.trishinfotech.builder;

public class SportsCarBuilder implements CarBuilder {

    private String chassis;
    private String body;
    private String paint;
    private String interior;

    public SportsCarBuilder() {
        super();
    }

    @Override
    public CarBuilder fixChassis() {
        System.out.println("Assembling chassis of the sports model");
        this.chassis = "Sporty Chassis";
        return this;
    }
 
    @Override
    public CarBuilder fixBody() {
        System.out.println("Assembling body of the sports model");
        this.body = "Sporty Body";
        return this;
    }
  
    @Override
    public CarBuilder paint() {
        System.out.println("Painting body of the sports model");
        this.paint = "Sporty Torch Red Paint";
        return this;
    }

    @Override
    public CarBuilder fixInterior() {
        System.out.println("Setting up interior of the sports model");
        this.interior = "Sporty interior";
        return this;
    }

    @Override
    public Car build() {
        Car car = new Car(chassis, body, paint, interior);
        if (car.doQualityCheck()) {
            return car;
        } else {
            System.out.println("Car assembly is incomplete. Can't deliver!");
        }
        return null;
    }

}

Шаг 4: Теперь мы напишем класс-распорядитель AutomotiveEngineer, под руководством которого строитель будет собирать автомобиль (объект Car) шаг за шагом в четко определенном порядке.

package org.trishinfotech.builder;

public class AutomotiveEngineer {

    private CarBuilder builder;

    public AutomotiveEngineer(CarBuilder builder) {
        super();
        this.builder = builder;
        if (this.builder == null) {
            throw new IllegalArgumentException("Automotive Engineer can't work without Car Builder!");
        }
    }

    public Car manufactureCar() {
        return builder.fixChassis().fixBody().paint().fixInterior().build();
    }

}

 Мы видим, что метод manufactureCar вызывает этапы сборки автомобиля в правильном порядке.

Теперь пришло время написать класс Main для выполнения и тестирования нашего кода.

package org.trishinfotech.builder;

public class Main {

    public static void main(String[] args) {
        CarBuilder builder = new SportsCarBuilder();
        AutomotiveEngineer engineer = new AutomotiveEngineer(builder);
        Car car = engineer.manufactureCar();
        if (car != null) {
            System.out.println("Below car delievered: ");
            System.out.println("======================================================================");
            System.out.println(car);
            System.out.println("======================================================================");
        }
    }

}

 Ниже приведен вывод программы:

Assembling chassis of the sports model
Assembling body of the sports model
Painting body of the sports model
Setting up interior of the sports model
Below car delievered: 
======================================================================
Car [chassis=Sporty Chassis, body=Sporty Body, paint=Sporty Torch Red Paint, interior=Sporty interior]
======================================================================

 Я надеюсь, что вы хорошо разобрались в объяснении и примере, чтобы понять паттерн Builder. Некоторые из нас также находят у него сходство с паттерном абстрактной фабрики (Abstract Factory), о котором я рассказывал в другой статье. Основное различие между строителем и абстрактной фабрикой состоит в том, что строитель предоставляет нам больший или лучший контроль над процессом создания объекта. Если вкратце, то паттерн абстрактной фабрики отвечает на вопрос «что», а паттерн строитель - «как».

 Исходный код можно найти здесь: Real-Builder-Design-Pattern-Source-Code

Я нашел паттерн Builder невероятно полезным и одним из наиболее часто используемых в приложениях в настоящее время. Я пришел к выводу, что Builder лучше подходит для работы с иммутабельными объектами. Все мы знаем, как много есть хороших иммутабельных объектов, и их использование увеличивается день ото дня, особенно после релиза Java 8.

Я использую Builder для написания своих сложных иммутабельных классов, и я бы хотел продемонстриовать здесь эту идею.

В качестве примера у нас есть класс Employee, в котором есть несколько полей.

public class Employee {

    private int empNo;
    private String name;
    private String depttName;
    private int salary;
    private int mgrEmpNo;
    private String projectName;
}

 Предположим, только два поля EmpNo и EmpName являются обязательными, а все остальные - опциональные. Поскольку это иммутабельный класс, у меня есть два варианта написания конструкторов.

  1. Написать конструктор с параметрами под все поля.

  2. Написать несколько конструкторов для разных комбинаций параметров, чтобы создать разные представления объекта Employee.

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

Employee emp1 = new Employee (100, "Brijesh", null, 0, 0, "Builder Pattern");

 Второй вариант тоже не очень хорош, так как мы создаем слишком много конструкторов.

  public Employee(int empNo, String name) {
        super();
        if (empNo <= 0) {
            throw new IllegalArgumentException("Please provide valid employee number.");
        }
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Please provide employee name.");
        }
        this.empNo = empNo;
        this.name = name;
    }

    public Employee(int empNo, String name, String depttName) {
        this(empNo, name);
        this.depttName = depttName;
    }

    public Employee(int empNo, String name, String depttName, int salary) {
        this(empNo, name, depttName);
        this.salary = salary;
    }

    public Employee(int empNo, String name, String depttName, int salary, int mgrEmpNo) {
        this(empNo, name, depttName, salary);
        this.mgrEmpNo = mgrEmpNo;
    }

    public Employee(int empNo, String name, String depttName, int salary, int mgrEmpNo, String projectName) {
        this(empNo, name, depttName, salary, mgrEmpNo);
        this.projectName = projectName;
    }

 Итак, вот решение с помощью паттерна Builder:

package org.trishinfotech.builder.example;

public class Employee {

    private int empNo;
    private String name;
    private String depttName;
    private int salary;
    private int mgrEmpNo;
    private String projectName;

    public Employee(EmployeeBuilder employeeBuilder) {
        if (employeeBuilder == null) {
            throw new IllegalArgumentException("Please provide employee builder to build employee object.");
        }
        if (employeeBuilder.empNo <= 0) {
            throw new IllegalArgumentException("Please provide valid employee number.");
        }
        if (employeeBuilder.name == null || employeeBuilder.name.trim().isEmpty()) {
            throw new IllegalArgumentException("Please provide employee name.");
        }
        this.empNo = employeeBuilder.empNo;
        this.name = employeeBuilder.name;
        this.depttName = employeeBuilder.depttName;
        this.salary = employeeBuilder.salary;
        this.mgrEmpNo = employeeBuilder.mgrEmpNo;
        this.projectName = employeeBuilder.projectName;
    }

    public int getEmpNo() {
        return empNo;
    }

    public String getName() {
        return name;
    }
    
    public String getDepttName() {
        return depttName;
    }

    public int getSalary() {
        return salary;
    }

    public int getMgrEmpNo() {
        return mgrEmpNo;
    }
    
    public String getProjectName() {
        return projectName;
    }

    @Override
    public String toString() {
        // Класс StringBuilder также использует паттерн проектирования Builder с реализацией
        // интерфейса java.lang.Appendable
        StringBuilder builder = new StringBuilder();
        builder.append("Employee [empNo=").append(empNo).append(", name=").append(name).append(", depttName=")
                .append(depttName).append(", salary=").append(salary).append(", mgrEmpNo=").append(mgrEmpNo)
                .append(", projectName=").append(projectName).append("]");
        return builder.toString();
    }

    public static class EmployeeBuilder {
        private int empNo;
        protected String name;
        protected String depttName;
        protected int salary;
        protected int mgrEmpNo;
        protected String projectName;

        public EmployeeBuilder() {
            super();
        }
        
        public EmployeeBuilder empNo(int empNo) {
            this.empNo = empNo;
            return this;
        }

        public EmployeeBuilder name(String name) {
            this.name = name;
            return this;
        }

        public EmployeeBuilder depttName(String depttName) {
            this.depttName = depttName;
            return this;
        }

        public EmployeeBuilder salary(int salary) {
            this.salary = salary;
            return this;
        }

        public EmployeeBuilder mgrEmpNo(int mgrEmpNo) {
            this.mgrEmpNo = mgrEmpNo;
            return this;
        }
        public EmployeeBuilder projectName(String projectName) {
            this.projectName = projectName;
            return this;
        }

        public Employee build() {
            Employee emp = null;
            if (validateEmployee()) {
                emp = new Employee(this);
            } else {
                System.out.println("Sorry! Employee objects can't be build without required details");
            }
            return emp;
        }

        private boolean validateEmployee() {
           return (empNo > 0 && name != null && !name.trim().isEmpty());
        }
    }
}

Я написал EmployeeBuilder как публичный статический вложенный класс. Вы можете написать его как обычный публичный класс в отдельном файл Java. Большой разницы я не вижу.

Теперь напишем программу EmployeeMain для создания объекта Employee:

package org.trishinfotech.builder.example;

public class EmployeeMain {

    public static void main(String[] args) {
        Employee emp1 = new Employee.EmployeeBuilder().empNo(100).name("Brijesh").projectName("Builder Pattern")
                .build();
        System.out.println(emp1);
    }
}

Надеюсь, вам понравилась идея. Мы можем использовать это при создании более сложных объектов. Я не реализовал здесь распорядителя (Director), так как все шаги (сбор значений для полей) не являются обязательными и могут выполняться в любом порядке. Чтобы убедиться, что я создаю объект Employee только после получения всех обязательных полей, я написал метод проверки.

Пример с оформлением заказа в ресторане с использованием паттерна Builder

Я хочу еще показать вам пример кода для оформления заказа в ресторане, где Order (заказ) является иммутабельным объектом и требует тип обслуживания заказа - Order Service Type (Take Away - с собой/Eat Here - в заведении), всех необходимых нам продуктов питания (Food Items) и имени клиента (Customer Name - опционально) в время оформления заказа. Продуктов питания может быть сколько угодно. Итак, вот код этого примера.

Код для перечисления OrderService:

package org.trishinfotech.builder;

public enum OrderService {

    TAKE_AWAY("Take Away", 2.0d), EAT_HERE("Eat Here", 5.5d);

    private String name;
    private double tax;

    OrderService(String name, double tax) {
        this.name = name;
        this.tax = tax;
    }

    public String getName() {
        return name;
    }

    public double getTax() {
        return tax;
    }

}

 Код для интерфейса FoodItem:

package org.trishinfotech.builder.meal;

import org.trishinfotech.builder.packing.Packing;

public interface FoodItem {

    public String name();

    public int calories();

    public Packing packing();

    public double price();
}

 Код для класса Meal (блюдо). Класс Meal предлагает заранее определенные продукты питания со скидкой на цену товара (не на цену упаковки).

package org.trishinfotech.builder.meal;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.trishinfotech.builder.packing.MultiPack;
import org.trishinfotech.builder.packing.Packing;

public class Meal implements FoodItem {

    private List<FoodItem> foodItems = new ArrayList<FoodItem>();
    private String mealName;
    private double discount;

    public Meal(String mealName, List<FoodItem> foodItems, double discount) {
        super();
        if (Objects.isNull(foodItems) || foodItems.stream().filter(Objects::nonNull).collect(Collectors.toList()).isEmpty()) {
            throw new IllegalArgumentException(
                    "Meal can't be order without any food item");
        }
        this.mealName = mealName;
        this.foodItems = new ArrayList<FoodItem>(foodItems);
        this.discount = discount;
    }

    public List<FoodItem> getFoodItems() {
        return foodItems;
    }

    @Override
    public String name() {
        return mealName;
    }
  
    @Override
    public int calories() {
        int totalCalories = foodItems.stream().mapToInt(foodItem -> foodItem.calories()).sum();
        return totalCalories;
    }

    @Override
    public Packing packing() {
        double packingPrice = foodItems.stream().map(foodItem -> foodItem.packing())
                .mapToDouble(packing -> packing.packingPrice()).sum();
        return new MultiPack(packingPrice);
    }

    @Override
    public double price() {
        double totalPrice = foodItems.stream().mapToDouble(foodItem -> foodItem.price()).sum();
        return totalPrice;
    }

    public double discount() {
        return discount;
    }
}

Еда:

Код для класса Burger:

package org.trishinfotech.builder.food.burger;

import org.trishinfotech.builder.meal.FoodItem;
import org.trishinfotech.builder.packing.Packing;
import org.trishinfotech.builder.packing.Wrap;

public abstract class Burger implements FoodItem {

    @Override
    public Packing packing() {
        return new Wrap();
    }

}

Код для класса ChickenBurger:

package org.trishinfotech.builder.food.burger;

public class ChickenBurger extends Burger {

    @Override
    public String name() {
        return "Chicken Burger";
    }

    @Override
    public int calories() {
        return 300;
    }

    @Override
    public double price() {
        return 4.5d;
    }

}

Код для класса VegBurger (веганский бургер):

package org.trishinfotech.builder.food.burger;

public class VegBurger extends Burger {

    @Override
    public String name() {
        return "Veg Burger";
    }

    @Override
    public int calories() {
        return 180;
    }

    @Override
    public double price() {
        return 2.7d;
    }

}

Код для класса Nuggets:

package org.trishinfotech.builder.food.nuggets;

import org.trishinfotech.builder.meal.FoodItem;
import org.trishinfotech.builder.packing.Container;
import org.trishinfotech.builder.packing.Packing;

public abstract class Nuggets implements FoodItem {

    @Override
    public Packing packing() {
        return new Container();
    }

}

 Код для класса CheeseNuggets:

package org.trishinfotech.builder.food.nuggets;

public class CheeseNuggets extends Nuggets {

    @Override
    public String name() {
        return "Cheese Nuggets";
    }

    @Override
    public int calories() {
        return 330;
    }

    @Override
    public double price() {
        return 3.8d;
    }

}

Код для класса ChickenNuggets:

package org.trishinfotech.builder.food.nuggets;

public class ChickenNuggets extends Nuggets {

    @Override
    public String name() {
        return "Chicken Nuggets";
    }

    @Override
    public int calories() {
        return 450;
    }

    @Override
    public double price() {
        return 5.0d;
    }

}

Напитки:

Напитки бывают разных размеров. Итак, вот код перечисления BeverageSize:

package org.trishinfotech.builder.beverages;

public enum BeverageSize {
    XS("Extra Small", 110), S("Small", 150), M("Medium", 210), L("Large", 290);
    private String name;
    private int calories;

    BeverageSize(String name, int calories) {
        this.name = name;
        this.calories = calories;
    }
  
    public String getName() {
        return name;
    }

    public int getCalories() {
        return calories;
    }

}

Код для класса Drink:

package org.trishinfotech.builder.beverages;

import org.trishinfotech.builder.meal.FoodItem;

public abstract class Drink implements FoodItem {

    protected BeverageSize size;

    public Drink(BeverageSize size) {
        super();
        this.size = size;
        if (this.size == null) {
            this.size = BeverageSize.M;
        }
    }

    public BeverageSize getSize() {
        return size;
    }

    public String drinkDetails() {
        return " (" + size + ")";
    }
}

 Код для класса ColdDrink:

package org.trishinfotech.builder.beverages.cold;

import org.trishinfotech.builder.beverages.BeverageSize;
import org.trishinfotech.builder.beverages.Drink;
import org.trishinfotech.builder.packing.Bottle;
import org.trishinfotech.builder.packing.Packing;

public abstract class ColdDrink extends Drink {

    public ColdDrink(BeverageSize size) {
        super(size);
    }

    @Override public Packing packing() {
        return new Bottle();
    }
}

 Код для класса CocaCola:

package org.trishinfotech.builder.beverages.cold;

import org.trishinfotech.builder.beverages.BeverageSize;

public class CocaCola extends ColdDrink {

    public CocaCola(BeverageSize size) {
        super(size);
    }

    @Override
    public String name() {
        return "Coca-Cola" + drinkDetails();
    }

    @Override
    public int calories() {
        if (size != null) {
            switch (size) {
            case XS:
                return 110;
            case S:
                return 150;
            case M:
                return 210;
            case L:
                return 290;
            default:
                break;
            }
        }
        return 0;
    }

    @Override
    public double price() {
        if (size != null) {
            switch (size) {
            case XS:
                return 0.80d;
            case S:
                return 1.0d;
            case M:
                return 1.5d;
            case L:
                return 2.0d;
            default:
                break;
            }
        }
        return 0.0d;
    }

}

Код для класса Pepsi:

package org.trishinfotech.builder.beverages.cold;

import org.trishinfotech.builder.beverages.BeverageSize;

public class Pepsi extends ColdDrink {

    public Pepsi(BeverageSize size) {
        super(size);
    }

    @Override public String name() {
        return "Pepsi" + drinkDetails();
    }

    @Override public int calories() {
        if (size != null) {
            switch (size) {
                case S:
                    return 160;
                case M:
                    return 220;
                case L:
                    return 300;
                default:
                    break;
            }
        }
        return 0;
    }

    @Override public double price() {
        if (size != null) {
            switch (size) {
                case S:
                    return 1.2d;
                case M:
                    return 2.2d;
                case L:
                    return 2.7d;
                default:
                    break;
            }
        }
        return 0.0d;
    }

}

 Код для класса HotDrink:

package org.trishinfotech.builder.beverages.hot;

import org.trishinfotech.builder.beverages.BeverageSize;
import org.trishinfotech.builder.beverages.Drink;
import org.trishinfotech.builder.packing.Packing;
import org.trishinfotech.builder.packing.SipperMug;

public abstract class HotDrink extends Drink {

    public HotDrink(BeverageSize size) {
        super(size);
    }
    
    @Override public Packing packing() {
        return new SipperMug();
    }
}

Код для класса Cuppuccinno:

package org.trishinfotech.builder.beverages.hot;

import org.trishinfotech.builder.beverages.BeverageSize;

public class Cappuccino extends HotDrink {

    public Cappuccino(BeverageSize size) {
        super(size);
    }

    @Override public String name() {
        return "Cappuccino" + drinkDetails();
    }
  
    @Override public int calories() {
        if (size != null) {
            switch (size) {
                case S:
                    return 120;
                case M:
                    return 160;
                case L:
                    return 210;
                default:
                break;
            }
        }
        return 0;
    }

    @Override public double price() {
        if (size != null) {
            switch (size) {
                case S:
                    return 1.0d;
                case M:
                    return 1.4d;
                case L:
                    return 1.8d;
                default:
                break;
            }
        }
        return 0.0d;
    }

}

Код для класса HotChocolate:

package org.trishinfotech.builder.beverages.hot;

import org.trishinfotech.builder.beverages.BeverageSize;

public class HotChocolate extends HotDrink {

    public HotChocolate(BeverageSize size) {
        super(size);
    }

    @Override public String name() {
        return "Hot Chocolate" + drinkDetails();
    }
  
    @Override public int calories() {
        if (size != null) {
            switch (size) {
                case S:
                    return 370;
                case M:
                    return 450;
                case L:
                    return 560;
                default:
                    break;
            }
        }
        return 0;
    }
    
    @Override public double price() {
        if (size != null) {
            switch (size) {
                case S:
                    return 1.6d;
                case M:
                    return 2.3d;
                case L:
                    return 3.0d;
                default:
                    break;
            }
          
       }
        return 0.0d;
    }

}

Упаковка:

Код интерфейса Packing:

package org.trishinfotech.builder.packing;

public interface Packing {

    public String pack();

    public double packingPrice();
}

Код для класса Bottle:

package org.trishinfotech.builder.packing;

public class Bottle implements Packing {

    @Override
    public String pack() {
        return "Bottle";
    }

    @Override
    public double packingPrice() {
        return 0.75d;
    }

}

Код для класса Container:

package org.trishinfotech.builder.packing;

public class Container implements Packing {

    @Override
    public String pack() {
        return "Container";
    }

    @Override
    public double packingPrice() {
        return 1.25d;
    }

}

Код для класса MultiPack. Упаковка MutiPack служит вспомогательной упаковкой для еды, когда мы используем разные упаковки для разных продуктов.

package org.trishinfotech.builder.packing;

public class MultiPack implements Packing {

    private double packingPrice;


    public MultiPack(double packingPrice) {
        super();
        this.packingPrice = packingPrice;
    }
  
    @Override
    public String pack() {
        return "Multi-Pack";
    }

    @Override
    public double packingPrice() {
        return packingPrice;
    }

}

Код для класса SipperMug:

package org.trishinfotech.builder.packing;

public class SipperMug implements Packing {

    @Override
    public String pack() {
        return "Sipper Mug";
    }

    @Override
    public double packingPrice() {
        return 1.6d;
    }

}

Код для класса Wrap:

package org.trishinfotech.builder.packing;

public class Wrap implements Packing {

    @Override
    public String pack() {
        return "Wrap";
    }

    @Override
    public double packingPrice() {
        return 0.40d;
    }

}

Код служебного класса BillPrinter, который я написал для печати детализированного счета.

package org.trishinfotech.builder.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.DoubleAdder;

import org.trishinfotech.builder.Order;
import org.trishinfotech.builder.OrderService;
import org.trishinfotech.builder.meal.Meal;
import org.trishinfotech.builder.packing.Packing;

public class BillPrinter {

    static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

    public static void printItemisedBill(Order order) {
        OrderService service = order.getService();
        System.out.printf("%60s\n", "Food Court");
        System.out.println("=================================================================================================================");
        System.out.printf("Service: %10s (%2.2f Tax)                                                         Customer Name: %-20s\n", service.getName(), service.getTax(), order.getCustomerName());
        System.out.println("-----------------------------------------------------------------------------------------------------------------");
        System.out.printf("%25s | %10s | %10s | %10s | %15s | %10s | %10s\n", "Food Item", "Calories", "Packing", "Price", "Packing Price", "Discount %", "Total Price");
        System.out.println("-----------------------------------------------------------------------------------------------------------------");
        DoubleAdder itemTotalPrice = new DoubleAdder();
        order.getFoodItems().stream().forEach(item -> {
            String name = item.name();
            int calories = item.calories();
            Packing packing = item.packing();
            double price = item.price();
            double packingPrice = packing.packingPrice();
            double discount = item instanceof Meal? ((Meal)item).discount() : 0.0d;
            double totalItemPrice = calculateTotalItemPrice(price, packingPrice, discount);
            System.out.printf("%25s | %10d | %10s | %10.2f | %15.2f | %10.2f | %10.2f\n", name, calories, packing.pack(), price, packing.packingPrice(), discount, totalItemPrice);
            itemTotalPrice.add(totalItemPrice);
        });
        System.out.println("=================================================================================================================");
        double billTotal = itemTotalPrice.doubleValue();
        billTotal = applyTaxes(billTotal, service);
        System.out.printf("Date: %-30s %66s %.2f\n", dtf.format(LocalDateTime.now()), "Total Bill (incl. taxes):", billTotal);
        System.out.println("Enjoy your meal!\n\n\n\n");
    }

    private static double applyTaxes(double billTotal, OrderService service) {
        return billTotal + (billTotal * service.getTax())/100;
    }

    private static double calculateTotalItemPrice(double price, double packingPrice, double discount) {
        if (discount > 0.0d) {
            price = price - (price * discount)/100;
        }
        return price + packingPrice;
    }
}

Почти все готово. Пришло время написать наш иммутабельный класс Order:

package org.trishinfotech.builder;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.trishinfotech.builder.meal.FoodItem;

public class Order {

    private List<FoodItem> foodItems = new ArrayList<FoodItem>();

    private String customerName;
    private OrderService service;

    public Order(OrderService service, List<FoodItem> foodItems, String customerName) {
        super();
        if (Objects.isNull(service)) {
            throw new IllegalArgumentException(
                    "Meal can't be order without selecting service 'Take Away' or 'Eat Here'");
        }
        if (Objects.isNull(foodItems) || foodItems.stream().filter(Objects::nonNull).collect(Collectors.toList()).isEmpty()) {
            throw new IllegalArgumentException(
                    "Meal can't be order without any food item");
        }
        this.service = service;
        this.foodItems = new ArrayList<FoodItem>(foodItems);
        this.customerName = customerName;
        if (this.customerName == null) {
            this.customerName = "NO NAME";
        }
    }

    public List<FoodItem> getFoodItems() {
        return foodItems;
    }

    public String getCustomerName() {
        return customerName;
    }

    public OrderService getService() {
        return service;
    }

}

А вот код для OrderBuilder, который конструирует объект Order.

package org.trishinfotech.builder;

import java.util.ArrayList;
import java.util.List;

import org.trishinfotech.builder.beverages.BeverageSize;
import org.trishinfotech.builder.beverages.cold.CocaCola;
import org.trishinfotech.builder.beverages.cold.Pepsi;
import org.trishinfotech.builder.food.burger.ChickenBurger;
import org.trishinfotech.builder.food.burger.VegBurger;
import org.trishinfotech.builder.food.nuggets.CheeseNuggets;
import org.trishinfotech.builder.food.nuggets.ChickenNuggets;
import org.trishinfotech.builder.meal.FoodItem;
import org.trishinfotech.builder.meal.Meal;

public class OrderBuilder {

    protected static final double HAPPY_MENU_DISCOUNT = 5.0d;

    private String customerName;

    private OrderService service = OrderService.TAKE_AWAY;

    private List<FoodItem> items = new ArrayList<FoodItem>();

    public OrderBuilder() {
        super();
    }
  
    // Сеттеры для каждого поля в целевом объекте. В этом примере это Order.
    // Возвращаемым типом у нас будет сам Builder (например, OrderBuilder), чтобы сделать возможным цепной вызов сеттеров.
    public OrderBuilder name(String customerName) {
        this.customerName = customerName;
        return this;
    }
  
    public OrderBuilder service(OrderService service) {
        if (service != null) {
            this.service = service;
        }
        return this;
    }

    public OrderBuilder item(FoodItem item) {
        items.add(item);
        return this;
    }

    // Комбо предложения 
    public OrderBuilder vegNuggetsHappyMeal() {
        List<FoodItem> foodItems = new ArrayList<FoodItem>();
        foodItems.add(new CheeseNuggets());
        foodItems.add(new Pepsi(BeverageSize.S));
        Meal meal = new Meal("Veg Nuggets Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);
        return item(meal);
    }

    public OrderBuilder chickenNuggetsHappyMeal() {
        List<FoodItem> foodItems = new ArrayList<FoodItem>();
        foodItems.add(new ChickenNuggets());
        foodItems.add(new CocaCola(BeverageSize.S));
        Meal meal = new Meal("Chicken Nuggets Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);
        return item(meal);
    }

    public OrderBuilder vegBurgerHappyMeal() {
        List<FoodItem> foodItems = new ArrayList<FoodItem>();
        foodItems.add(new VegBurger());
        foodItems.add(new Pepsi(BeverageSize.S));
        Meal meal = new Meal("Veg Burger Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);
        return item(meal);
    }

    public OrderBuilder chickenBurgerHappyMeal() {
        List<FoodItem> foodItems = new ArrayList<FoodItem>();
        foodItems.add(new ChickenBurger());
        foodItems.add(new CocaCola(BeverageSize.S));
        Meal meal = new Meal("Chicken Burger Happy Meal", foodItems, HAPPY_MENU_DISCOUNT);
        return item(meal);
    }

    public Order build() {
        Order order = new Order(service, items, customerName);
        if (!validateOrder()) {
            System.out.println("Sorry! Order can't be placed without service type (Take Away/Eat Here) and any food item.");
            return null;
        }
        return order;
    }

    private boolean validateOrder() {
        return (service != null) && !items.isEmpty();
    }
}

Готово! Теперь пришло время написать Main для выполнения и тестирования результат:

package org.trishinfotech.builder;

import org.trishinfotech.builder.beverages.BeverageSize;
import org.trishinfotech.builder.beverages.cold.CocaCola;
import org.trishinfotech.builder.beverages.cold.Pepsi;
import org.trishinfotech.builder.beverages.hot.HotChocolate;
import org.trishinfotech.builder.food.burger.ChickenBurger;
import org.trishinfotech.builder.food.nuggets.CheeseNuggets;
import org.trishinfotech.builder.food.nuggets.ChickenNuggets;
import org.trishinfotech.builder.util.BillPrinter;

public class Main {

    public static void main(String[] args) {
        OrderBuilder builder1 = new OrderBuilder();
        // you can see the use of chained calls of setters here. No statement terminator
        // till we set all the values of the object
        Order meal1 = builder1.name("Brijesh").service(OrderService.TAKE_AWAY).item(new ChickenBurger())
                .item(new Pepsi(BeverageSize.M)).vegNuggetsHappyMeal().build();
        BillPrinter.printItemisedBill(meal1);

        OrderBuilder builder2 = new OrderBuilder();
        Order meal2 = builder2.name("Micheal").service(OrderService.EAT_HERE).item(new ChickenNuggets())
                .item(new CheeseNuggets()).item(new CocaCola(BeverageSize.L)).chickenBurgerHappyMeal()
                .item(new HotChocolate(BeverageSize.M)).vegBurgerHappyMeal().build();
        BillPrinter.printItemisedBill(meal2);
    }

}

 А вот и результат работы программы:


                                                  Food Court
=================================================================================================================
Service:  Take Away (2.00 Tax)                                                         Customer Name: Brijesh             
-----------------------------------------------------------------------------------------------------------------
                Food Item |   Calories |    Packing |      Price |   Packing Price | Discount % | Total Price
-----------------------------------------------------------------------------------------------------------------
           Chicken Burger |        300 |       Wrap |       4.50 |            0.40 |       0.00 |       4.90
                Pepsi (M) |        220 |     Bottle |       2.20 |            0.75 |       0.00 |       2.95
   Veg Nuggets Happy Meal |        490 | Multi-Pack |       5.00 |            2.00 |       5.00 |       6.75
=================================================================================================================
Date: 2020/10/09 20:02:38                                                     Total Bill (incl. taxes): 14.89
Enjoy your meal!

                                                  Food Court
=================================================================================================================
Service:   Eat Here (5.50 Tax)                                                         Customer Name: Micheal             
-----------------------------------------------------------------------------------------------------------------
                Food Item |   Calories |    Packing |      Price |   Packing Price | Discount % | Total Price
-----------------------------------------------------------------------------------------------------------------
          Chicken Nuggets |        450 |  Container |       5.00 |            1.25 |       0.00 |       6.25
           Cheese Nuggets |        330 |  Container |       3.80 |            1.25 |       0.00 |       5.05
            Coca-Cola (L) |        290 |     Bottle |       2.00 |            0.75 |       0.00 |       2.75
Chicken Burger Happy Meal |        450 | Multi-Pack |       5.50 |            1.15 |       5.00 |       6.38
        Hot Chocolate (M) |        450 | Sipper Mug |       2.30 |            1.60 |       0.00 |       3.90
    Veg Burger Happy Meal |        340 | Multi-Pack |       3.90 |            1.15 |       5.00 |       4.86
=================================================================================================================
Date: 2020/10/09 20:02:38                                                     Total Bill (incl. taxes): 30.78
Enjoy your meal!

Ну вот и все! Я надеюсь, что этот урок помог освоить паттерн Builder.

Исходный код можно найти здесь: Real-Builder-Design-Pattern-Source-Code

и здесь: Builder-Design-Pattern-Sample-Code


Узнать подробнее о курсе «Архитектура и шаблоны проектирования».

Смотреть вебинар «Шаблоны GRASP».