Сегодня речь пойдет о шаблоне проектирования 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;
        }
    }
}

Что здесь происходит? После выставления какого либо параметра возвращается объект класса, имеющего только один метод. Таким образом мы задаем порядок вызова:

  1. выставить имя
  2. выставить возраст
  3. создать объект

И что не маловажно мы добились того, что проверка порядка осуществляется на этапе компиляции.

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();