Паттерн проектирования Builder (Строитель) в Java

от автора

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

Приглашаем также всех желающих на открытый демо-урок «Шаблоны 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».

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/552412/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *