Сегодня речь пойдет о шаблоне проектирования Builder, или же по русски, строитель.
Начнем с определения и выясним для чего он нужен.
Строитель относится к порождающим шаблонам, а значит, с его помощью создаются объекты. Он позволяет конструировать объект по частям. Приведу пример, как выглядит типичный Builder.
Допустим у нас есть класс Person
public class Person {
public final String name;
public final int age;
public Person(String name, int age) {
this.age = age;
this.name = name;
}
}
Чтобы сконструировать объект данного класса требуется знать имя и возраст. Builder для этого класса представляет некий промежуточный объект, хранящий все эти параметры.
public class PersonBuilder {
private int age;
private String name;
public PersonBuilder setAge(int age) {
this.age = age;
return this;
}
public PersonBuilder setName(String name) {
this.name = name;
return this;
}
public static PersonBuilder builder() {
return new PersonBuilder();
}
public Person build() {
return new Person(name, age);
}
}
В данном случае, он выглядит несколько избыточным, но когда параметров становится больше и/или когда в момент создания требуется выполнить некоторые проверки либо дополнительные операции, то преимущества шаблона становятся более очевидными. Логика создания объектов класса Person
выносится из самого класса в класс-строителя. Тем самым мы не перегружаем Person
дополнительной функциональностью.
Создать объект теперь можно так:
Person person = PersonBuilder
.builder()
.setAge(26)
.setName("Artem")
.build();
Заметим, что порядок вызова методов для выставления параметров не задан. Не всегда такого подходит, например, в случае, если проверка значения параметра зависит от значения другого параметра. Тогда можно воспользоваться следующим приемом:
public class PersonBuilder {
private int age;
private String name;
public static PersonNameBuilder builder() {
return new PersonNameBuilder(new PersonBuilder());
}
public Person build() {
return new Person(name, age);
}
public static class PersonNameBuilder {
private final PersonBuilder builder;
private PersonNameBuilder(PersonBuilder builder) {
this.builder = builder;
}
public PersonAgeBuilder setName(String name) {
builder.name = name;
return new PersonAgeBuilder(builder);
}
}
public static class PersonAgeBuilder {
private final PersonBuilder builder;
private PersonAgeBuilder(PersonBuilder builder) {
this.builder = builder;
}
public PersonBuilder setAge(int age) {
builder.age = age;
return builder;
}
}
}
Что здесь происходит? После выставления какого либо параметра возвращается объект класса, имеющего только один метод. Таким образом мы задаем порядок вызова:
- выставить имя
- выставить возраст
- создать объект
И что не маловажно мы добились того, что проверка порядка осуществляется на этапе компиляции.
Person person = PersonBuilder
.builder()
.setAge(26) <-- ошибка компиляции
.setName("Artem")
.build();
Согласитесь, что данный код, выглядит не очень читабельно. Улучить восприятие можно переименовав методы, создавая конструкции похожиe на естественный язык, например:
Person man = Person
.withName("Artem")
.andSurname("Konovalov")
.Lives()
.in("Saratov")
.at("Moscovskay st.")
.Works()
.as("Programmer")
.foR("Some company")
.earning(1_000_000)
.inCurrency(DOLLARS)
.Hobbies()
.is("programming")
.and()
.is("sport");
Но это потребует больше кода и больше времени над продумываем api 🙂 Естественно, код реализующий подобное, здесь я приводить не буду.
Порой возникает потребность в наследовании строителей. Например, базовый класс расширяется, обрастая наследниками, хочется переиспользовать уже созданный для него строитель. Допустим, есть наследник с именем Citizen
:
public class Citizen extends Person {
public final String address;
public Citizen(String name, String surname, String address) {
super(name, surname);
this.address = address;
}
}
Базовый строитель имеет следующую структуру:
public abstract class BasePersonBuilder<B extends BasePersonBuilder<B, P>, P> {
protected String name;
protected String surname;
@SuppressWarnings("unchecked")
public B setSurname(String surname) {
this.surname = surname;
return (B) this;
}
@SuppressWarnings("unchecked")
public B setName(String name) {
this.name = name;
return (B) this;
}
public abstract P build();
}
Тогда строитель для класса Person
выглядит так:
class PersonBuilder extends BasePersonBuilder<PersonBuilder, Person> {
@Override
public Person build() {
return new Person(name, surname);
}
}
А для Citizen
так:
class CitizenBuilder extends BasePersonBuilder<CitizenBuilder, Citizen> {
private String address;
public CitizenBuilder setAddress(String address) {
this.address = address;
return this;
}
@Override
public Citizen build() {
return new Citizen(name, surname, address);
}
}
Создание объектов данных классов по прежнему осталось такое же простое, как и в предыдущих случаях:
Person person = new PersonBuilder()
.setName("Max")
.setSurname("Pain")
.build();
Citizen citizen = new CitizenBuilder()
.setAddress("USA, NY")
.setName("Join")
.setSurname("Smith")
.build();