Java8中如何实现建造者模式?

How to implement the builder pattern in Java 8?

在 Java 之前实现构建器模式 8 有很多乏味的、几乎重复的代码;构建器本身通常是样板代码。一些 duplicate code detectors 几乎将 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;
    }
}

构建器模式如何使用Java8实现?

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 方法,该方法将 return 调用它的 GenericBuilder 实例的新克隆。这很容易实现,因为构建器的状态只是 instantiatorinstanceModifiers 的列表。从那时起,两个构建器都可以用其他一些 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);
    }
}

不可变对象创建

要使用上述方案创建不可变对象,将不可变对象的状态提取到可变对象中,并使用实例化器和构建器对可变状态进行操作目的。然后,添加一个函数,该函数将为可变状态创建一个新的不可变实例。但是,这要求不可变对象要么像这样封装其状态,要么以这种方式更改(基本上将参数对象模式应用于其构造函数)。

这在某种程度上不同于在 java-8 之前使用的生成器。在那里,构建器本身是一个可变对象,它在最后创建了一个新实例。现在,我们将构建器保存在可变对象中的状态与构建器功能本身分开了。

本质上
停止编写样板构建器模式并使用 GenericBuilder.

提高工作效率

您可以查看 lombok project

针对您的情况

@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 在编译阶段完成,对开发人员是透明的。

我们可以使用Java8的Consumer函数接口来避免多个getter/setter方法。

使用 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);
    }
}

参考下面link了解不同示例的详细信息。

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/

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

免责声明:我是 post

的作者

我最近尝试重温 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)
);

优点:

  • 干净的生成器界面
  • 几乎没有样板代码
  • 构建器封装的很好
  • 很容易将目标的可选属性与强制属性分开class(可选属性在构建器中指定)
  • 目标 class 中不需要 setter(在 DDD 中,您通常不需要 setters)
  • 使用静态工厂方法创建目标的实例class(而不是使用 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
    }
}

建立在 之上,这里是构建器模式的 quasi-immutable 版本:

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 class 类似于:

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 实例,这可以方便地提供机会来避免违反封装,否则会暴露不可变 class 上的获取访问器方法,而无需必要性。

这只是 quasi-immutable,因为如果保留可变实例的句柄,则可以更改这些值。以下是违反不变性的方式:

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复制到一个新的对象引用中,这避免了复制所有成员变量,同时构建了一个真正不可变的实例想要的类型。
  • Clone. 在构造不可变实例时克隆 mutator,这需要在任何地方实现 Serializable 或使用 deep-copy library.
  • 库。Project Lombok, AutoValue, or 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 );
}