Как реализовать шаблон построителя в Java 8?
Часто я нахожу утомительным реализовывать шаблон компоновщика с настройками pre-java-8. Там всегда много почти дублированного кода. Сам строитель мог бы считаться образцом.
Фактически, существуют детекторы с дублированием кода, которые будут рассматривать почти каждый метод компоновщика, созданный с помощью средств, предшествующих java-8, как дубликат любого другого метода.
Итак, учитывая следующий класс и его сборщик до 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;
}
}
Как шаблон компоновщика должен быть реализован с использованием средств 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 раз. Там сам строитель был изменяемым объектом, который в конце создал новый экземпляр. Теперь у нас есть разделение состояния, которое конструктор сохраняет в изменяемом объекте, и самой функциональности компоновщика.
По сути
Прекратите писать шаблоны шаблонов и станьте продуктивными, используя GenericBuilder
,
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();
Отказ от ответственности: я автор поста
Вы можете проверить проект Ломбок
Для вашего случая
@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(){...}
}
}
Lombok делает это на этапе компиляции и является прозрачным для разработчиков.
Мы можем использовать потребительский функциональный интерфейс Java 8, чтобы избежать нескольких методов получения / установки.
См. Обновленный ниже код с интерфейсом Consumer.
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://dkbalachandar.wordpress.com/2017/08/31/java-8-builder-pattern-with-consumer-interface/
Основываясь на этом ответе, вот квази-неизменяемая версия шаблона построителя:
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Responsible for constructing objects that would otherwise require
* a long list of constructor parameters.
*
* @param <MT> The mutable definition for the type of object to build.
* @param <IT> The immutable definition for the type of object to build.
*/
public class GenericBuilder<MT, IT> {
/**
* Provides the methods to use for setting object properties.
*/
private final Supplier<MT> mMutable;
/**
* Calling {@link #build()} will instantiate the immutable instance using
* the mutator.
*/
private final Function<MT, IT> mImmutable;
/**
* Adds a modifier to call when building an instance.
*/
private final List<Consumer<MT>> mModifiers = new ArrayList<>();
/**
* Constructs a new builder instance that is capable of populating values for
* any type of object.
*
* @param mutator Provides methods to use for setting object properties.
*/
protected GenericBuilder(
final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
mMutable = mutator;
mImmutable = immutable;
}
/**
* Starting point for building an instance of a particular class.
*
* @param supplier Returns the instance to build.
* @param <MT> The type of class to build.
* @return A new {@link GenericBuilder} capable of populating data for an
* instance of the class provided by the {@link Supplier}.
*/
public static <MT, IT> GenericBuilder<MT, IT> of(
final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
return new GenericBuilder<>( supplier, immutable );
}
/**
* Registers a new value with the builder.
*
* @param consumer Accepts a value to be set upon the built object.
* @param value The value to use when building.
* @param <V> The type of value used when building.
* @return This {@link GenericBuilder} instance.
*/
public <V> GenericBuilder<MT, IT> with(
final BiConsumer<MT, V> consumer, final V value ) {
mModifiers.add( instance -> consumer.accept( instance, value ) );
return this;
}
/**
* Instantiates then populates the immutable object to build.
*
* @return The newly built object.
*/
public IT build() {
final var value = mMutable.get();
mModifiers.forEach( modifier -> modifier.accept( value ) );
mModifiers.clear();
return mImmutable.apply( value );
}
}
Пример использования:
final var caret = CaretPosition
.builder()
.with( CaretPosition.Mutator::setParagraph, 5 )
.with( CaretPosition.Mutator::setMaxParagraph, 10 )
.build();
Когда ссылка на мутатор освобождается, состояние возвращаемого объекта фактически неизменяемо. В
CaretPosition
класс напоминает:
public class CaretPosition {
public static GenericBuilder<CaretPosition.Mutator, CaretPosition> builder() {
return GenericBuilder.of( CaretPosition.Mutator::new, CaretPosition::new );
}
public static class Mutator {
private int mParagraph;
private int mMaxParagraph;
public void setParagraph( final int paragraph ) {
mParagraph = paragraph;
}
public void setMaxParagraph( final int maxParagraph ) {
mMaxParagraph = maxParagraph;
}
}
private final Mutator mMutator;
private CaretPosition( final Mutator mutator ) {
mMutator = mutator;
}
// ...
Отсюда
CaretPosition
может свободно ссылаться на свои внутренние
Mutator
instance, который легко предоставляет возможность избежать нарушения инкапсуляции, в противном случае открывая методы доступа get в неизменяемом классе без необходимости.
Это только квази-неизменяемый, потому что значения могут быть изменены, если дескриптор изменяемого экземпляра сохраняется. Вот как может быть нарушена неизменность:
final var mutable = CaretPosition.builder()
.with( CaretPosition.Mutator::setParagraph, 5 )
.with( CaretPosition.Mutator::setMaxParagraph, 10 );
final var caret = mutable.build();
mutable.setParagraph( 17 );
System.out.println( "caret para: " + caret.toString() );
Должен
caret.toString()
Включите значение абзаца, результирующая строка будет содержать значение 17 вместо 5, что нарушит неизменяемость. Еще одним недостатком этого подхода является то, что если проверка выполняется в
build()
время, второй звонок
setParagraph
не будет проходить через валидатор.
Способы избежать этого включают:
- Конструктор неизменяемой копии. Скопируйте изменяемые переменные-члены в неизменяемый экземпляр, что влечет за собой дублирование всех переменных-членов.
Mutator
конструктор копирования. СкопируйтеMutator
в новую ссылку на объект, что позволяет избежать дублирования всех переменных-членов при создании действительно неизменяемого экземпляра желаемого типа.- Клонировать. Клонируйте мутатор при создании неизменяемого экземпляра, что требует либо реализации
Serializable
везде или с помощью библиотеки глубоких копий. - Библиотека. Отмените это решение для Project Lombok, AutoValue или Immutables.
В
Mutator
вариант конструктора копирования будет напоминать:
private Mutator() {
}
private Mutator( final Mutator mutator) {
mParagraph = mutator.mParagraph;
mMaxParagraph = mutator.mMaxParagraph;
}
Затем изменение на
CaretPosition
тривиально --- создайте экземпляр
Mutator
используя свой конструктор копирования:
private CaretPosition( final Mutator mutator ) {
mMutator = new Mutator( mutator );
}
Недавно я попытался вернуться к шаблону компоновщика в 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)
);
Преимущества:
- Интерфейс чистого строителя
- Немного, чтобы не шаблонный код
- Строитель хорошо инкапсулирован
- Легко отделить необязательные атрибуты от обязательных атрибутов целевого класса (необязательные атрибуты указываются в компоновщике)
- Нет необходимости в установщике в целевом классе (в DDD обычно не нужны сеттеры)
- Использование статического метода фабрики для создания экземпляра целевого класса (вместо использования ключевого слова new, поэтому возможно иметь несколько статических методов фабрики, каждый со значимым именем)
Возможные недостатки:
- Вызывающий код может сохранить ссылку на переданный компоновщик, а затем испортить смонтированный экземпляр, но кто это сделает?
- Если вызывающий код сохраняет ссылку на переданный компоновщик, может произойти утечка памяти
Возможная альтернатива:
Мы можем настроить конструктор с помощью функции здания следующим образом:
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
}
}