Как реализовать шаблон builder в Java 8?

часто мне кажется утомительным реализовывать шаблон builder с настройками pre-java-8. Всегда есть много почти дублированного кода. Сам строитель мог считаться шаблоном.

на самом деле есть код дублировать извещатели, это рассматривало бы почти каждый метод строителя, сделанный с объектами pre-java-8, как дубликат каждого другого метода.

поэтому, учитывая следующий класс, и это pre-java-8 строитель:

public class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class PersonBuilder {

    private static class PersonState {
        public String name;
        public int age;
    }

    private PersonState  state = new PersonState();

    public PersonBuilder withName(String name) {
        state.name = name;
        return this;
    }

    public PersonBuilder withAge(int age) {
        state.age = age;
        return this;
    }

    public Person build() {
        Person person = new Person();
        person.setAge(state.age);
        person.setName(state.name);
        state = new PersonState();
        return person;
    }
}

как шаблон builder должен быть реализован с использованием средств java-8?

5 ответов


на GenericBuilder

идея строительства изменяемые объекты (неизменяемые объекты рассматриваются позже) - использовать ссылки на методы для сеттеров экземпляра, который должен быть построен. Это приводит нас к общему построителю, который способен строить каждый POJO с конструктором по умолчанию-один строитель, чтобы управлять ими всеми; -)

реализация этого:

public class GenericBuilder<T> {

    private final Supplier<T> instantiator;

    private List<Consumer<T>> instanceModifiers = new ArrayList<>();

    public GenericBuilder(Supplier<T> instantiator) {
        this.instantiator = instantiator;
    }

    public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
        return new GenericBuilder<T>(instantiator);
    }

    public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
        Consumer<T> c = instance -> consumer.accept(instance, value);
        instanceModifiers.add(c);
        return this;
    }

    public T build() {
        T value = instantiator.get();
        instanceModifiers.forEach(modifier -> modifier.accept(value));
        instanceModifiers.clear();
        return value;
    }
}

строитель построен с поставщиком, который создает новые экземпляры, а затем эти экземпляры изменяются изменениями, указанными с помощью with метод.

на GenericBuilder будет использоваться для Person такой:

Person value = GenericBuilder.of(Person::new)
            .with(Person::setName, "Otto").with(Person::setAge, 5).build();

свойства и дальнейшее использование

но есть больше об этом Строителе, чтобы обнаружить.

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

продолжая эту мысль, мы могли бы реализовать fork метод, который вернет новый клон GenericBuilder экземпляр, который вызывается. Это легко возможно, потому что состояние строителя-это просто instantiator и список instanceModifiers. С этого момента оба строителя могут быть изменено с какой-то другой instanceModifiers. Они будут использовать одну и ту же основу и иметь некоторые дополнительные состояния, установленные для построенных экземпляров.

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

на GenericBuilder может также заменить потребность для различных фабрик значения теста. В моем текущем проекте, много фабрик используемых для создание тестовых экземпляров. Код тесно связан с различными сценариями тестирования, и трудно извлечь части тестовой фабрики для повторного использования на другой тестовой фабрике в немного другом сценарии. С GenericBuilder, повторное использование этого становится намного проще, поскольку существует только определенный список instanceModifiers.

чтобы убедиться, что созданные экземпляры действительны,GenericBuilder может быть инициализирован набором предикатов, которые проверяются в build метод после того, как все instanceModifiers несколько бежать.

public T build() {
    T value = instantiator.get();
    instanceModifiers.forEach(modifier -> modifier.accept(value));
    verifyPredicates(value);
    instanceModifiers.clear();
    return value;
}

private void verifyPredicates(T value) {
    List<Predicate<T>> violated = predicates.stream()
            .filter(e -> !e.test(value)).collect(Collectors.toList());
    if (!violated.isEmpty()) {
        throw new IllegalStateException(value.toString()
                + " violates predicates " + violated);
    }
}

создание неизменяемого объекта

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

это каким-то образом отличается от того, что строитель использовался в pre-java-8 раз. Там сам построитель был изменяемым объектом, который создал новый экземпляр в конце. Теперь у нас есть разделение состояния, которое строитель сохраняет в изменяемом объекте, и самой функциональности строителя.

по сути
Прекратите писать шаблоны boilerplate builder и получите продуктивную с помощью GenericBuilder.


Вы можете проверить проект Lombok

для вашего случая

@Builder
public class Person {
    private String name;
    private int age;
}

Он будет генерировать код на лету

public class Person {
    private String name;
    private int age;
    public String getName(){...}
    public void setName(String name){...}
    public int getAge(){...}
    public void setAge(int age){...}
    public Person.Builder builder() {...}

    public static class Builder {
         public Builder withName(String name){...}
         public Builder withAge(int age){...}
         public Person build(){...}
    }        
}

Ломбок делает это на этапе компиляции и прозрачен для разработчиков.


public class PersonBuilder {
    public String salutation;
    public String firstName;
    public String middleName;
    public String lastName;
    public String suffix;
    public Address address;
    public boolean isFemale;
    public boolean isEmployed;
    public boolean isHomewOwner;

    public PersonBuilder with(
        Consumer<PersonBuilder> builderFunction) {
        builderFunction.accept(this);
        return this;
    }


    public Person createPerson() {
        return new Person(salutation, firstName, middleName,
                lastName, suffix, address, isFemale,
                isEmployed, isHomewOwner);
    }
}

использование

Person person = new PersonBuilder()
    .with($ -> {
        $.salutation = "Mr.";
        $.firstName = "John";
        $.lastName = "Doe";
        $.isFemale = false;
    })
    .with($ -> $.isHomewOwner = true)
    .with($ -> {
        $.address =
            new PersonBuilder.AddressBuilder()
                .with($_address -> {
                    $_address.city = "Pune";
                    $_address.state = "MH";
                    $_address.pin = "411001";
                }).createAddress();
    })
    .createPerson();

см.: https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

отказ от ответственности: я автор поста


мы можем использовать потребительский функциональный интерфейс Java 8, чтобы избежать нескольких методов геттера/сеттера.

см. ниже обновленный код с потребительским интерфейсом.

import java.util.function.Consumer;

public class Person {

    private String name;

    private int age;

    public Person(Builder Builder) {
        this.name = Builder.name;
        this.age = Builder.age;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Person{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append('}');
        return sb.toString();
    }

    public static class Builder {

        public String name;
        public int age;

        public Builder with(Consumer<Builder> function) {
            function.accept(this);
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    public static void main(String[] args) {
        Person user = new Person.Builder().with(userData -> {
            userData.name = "test";
            userData.age = 77;
        }).build();
        System.out.println(user);
    }
}

см. ниже ссылку на подробную информацию с разных образцы.

https://medium.com/beingprofessional/think-functional-advanced-builder-pattern-using-lambda-284714b85ed5

https://dkbalachandar.wordpress.com/2017/08/31/java-8-builder-pattern-with-consumer-interface/


недавно я попытался пересмотреть шаблон builder в Java 8, и в настоящее время я использую следующий подход:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person().build(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person() {

    }

    private Person build(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants

        return this;
    }
}

public interface PersonBuilder {

    PersonBuilder withName(String name);

    PersonBuilder withAge(int age);
}

использование:

var person = Person.create(
    personBuilder -> personBuilder.withName("John Smith").withAge(43)
);

плюсы:

  • чистый интерфейс builder
  • мало не шаблонный код
  • строитель хорошо инкапсулирован
  • легко отделить необязательные атрибуты от обязательных атрибутов целевого класса (необязательный атрибуты указываются в построителе)
  • нет сеттера, необходимого в целевом классе (в DDD вы обычно не хотите сеттеров)
  • использование статического Заводского метода для создания экземпляра целевого класса (вместо использования нового ключевого слова, поэтому можно иметь несколько статических заводских методов, каждый со значимым именем)

возможные недостатки:

  • вызывающий код может сохранить ссылку на переданный строитель и позже испортит установленный экземпляр, но кто это сделает?
  • если вызывающий код сохраняет ссылку на переданный построитель, может произойти утечка памяти

возможный вариант:

мы можем настроить конструктор со строительной функцией следующим образом:

public class Person {

    static public Person create(Consumer<PersonBuilder> buildingFunction) {
        return new Person(buildingFunction);
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    private Person(Consumer<PersonBuilder> buildingFunction) {
        buildingFunction.accept(new PersonBuilder() {

            @Override
            public PersonBuilder withName(String name) {
                Person.this.name = name;
                return this;
            }

            @Override
            public PersonBuilder withAge(int age) {
                Person.this.age = age;
                return this;
            }
        });

        if (name == null || name.isEmpty()) {
            throw new IllegalStateException("the name must not be null or empty");
        }

        if (age <= 0) {
            throw new IllegalStateException("the age must be > 0");
        }

        // check other invariants
    }
}