与龙目岛@Builder 的必要参数

如果我将 @ Builder添加到类中。

Person.builder().name("john").surname("Smith").build();

我有一个需要特定领域的需求。在这种情况下,需要 name 字段,但不需要 name。理想情况下,我想这样声明。

Person.builder("john").surname("Smith").build()

我不知道该怎么做。我试过将@Builder 添加到构造函数中,但是没有用。

@Builder
public Person(String name) {
this.name = name;
}
106443 次浏览

You can do it easily with Lombok annotation configuration

import lombok.Builder;
import lombok.ToString;


@Builder(builderMethodName = "hiddenBuilder")
@ToString
public class Person {


private String name;
private String surname;


public static PersonBuilder builder(String name) {
return hiddenBuilder().name(name);
}
}

And then use it like that

Person p = Person.builder("Name").surname("Surname").build();
System.out.println(p);

Of course @ToString is optional here.

Here's another approach:

@Builder()
@Getter
@ToString
public class Person {


private final String name;
private final String surname;


public static PersonBuilder builder(String name){
return new PersonBuilder().name(name);
}


public static void main(String[] args) {
Person p = Person.builder("John Doe")
.surname("Bill")
.build();
}
}

I would recommend against this approach, as you will struggle to apply it consistently on other objects. Instead, you can just mark fields with @lombok.NonNull annotation and Lombok will generate null checks for you in the constructor and setters, so that Builder.build() will fail, if those fields are not set.

Using builder pattern allows you to have very clear identification of which fields you're setting to which values. This is already lost for name field in your example, and it will further be lost for all other required fields, if you're building an object with multiple required fields. Consider the following example, can you tell which field is which just by reading code?

Person.builder("John", "Michael", 16, 1987) // which is name, which is surname? what is 16?
.year(1982) // if this is year of birth, then what is 1987 above?
.build()

This is my solution for the problem

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;


@Data
@Builder(builderMethodName = "privateBuilder")
public class Person {
@NonNull
private String name;
@NonNull
private String surname;
private int age;//optional


public static Url safeBuilder() {
return new Builder();
}


interface Url {
Surname name(String name);
}


interface Surname {
Build surname(String surname);
}


interface Build {
Build age(int age);
Person build();
}


public static class Builder implements Url, Surname, Build {
PersonBuilder pb = Person.privateBuilder();


@Override
public Surname name(String name) {
pb.name(name);
return this;
}


@Override
public Build surname(String surname) {
pb.surname(surname);
return this;


}


@Override
public Build age(int age) {
pb.age(age);
return this;
}


@Override
public Person build() {
return pb.build();
}
}
}

inspired by this blog post:

https://blog.jayway.com/2012/02/07/builder-pattern-with-a-twist/

The simpliest solution is to add a @lombok.NonNull to all mandatory values. The Builder will fail to build the object when mandatory fields are not set.

Here's a JUnit test to demonstrate the behavior of all combinations of final and @NonNull:

import static org.junit.Assert.fail;


import org.junit.Test;


import lombok.Builder;
import lombok.ToString;


public class BuilderTests {
@Test
public void allGiven() {
System.err.println(Foo.builder()
.nonFinalNull("has_value")
.nonFinalNonNull("has_value")
.finalNull("has_value")
.finalNonNull("has_value")
.build());
}


@Test
public void noneGiven() {
try {
System.err.println(Foo.builder()
.build()
.toString());
fail();
} catch (NullPointerException e) {
// expected
}
}


@Test
public void nonFinalNullOmitted() {
System.err.println(Foo.builder()
.nonFinalNonNull("has_value")
.finalNull("has_value")
.finalNonNull("has_value")
.build());
}


@Test
public void nonFinalNonNullOmitted() {
try {
System.err.println(Foo.builder()
.nonFinalNull("has_value")
.finalNull("has_value")
.finalNonNull("has_value")
.build());
fail();
} catch (NullPointerException e) {
// expected
}
}


@Test
public void finalNullOmitted() {
System.err.println(Foo.builder()
.nonFinalNull("has_value")
.nonFinalNonNull("has_value")
.finalNonNull("has_value")
.build());
}


@Test
public void finalNonNullOmitted() {
try {
System.err.println(Foo.builder()
.nonFinalNull("has_value")
.nonFinalNonNull("has_value")
.finalNull("has_value")
.build());
fail();
} catch (NullPointerException e) {
// expected
}
}


@Builder
@ToString
private static class Foo {
private String nonFinalNull;


@lombok.NonNull
private String nonFinalNonNull;


private final String finalNull;


@lombok.NonNull
private final String finalNonNull;
}
}

Taking Kevin Day's answer a step further:

@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE) // If immutability is desired
@ToString
public class Person {
@NonNull // Presumably name cannot be null since its required by the builder
private final String name;
private final String surname;


private static PersonBuilder builder() {
return new PersonBuilder();
}


public static PersonBuilder builder(String name){
return builder().name(name);
}


}

It's not ideal, but it provides compile time enforcement and callers of this class will have exactly one builder method to use.

If you need this functionality, you can customize the builder class by yourself and you can still add @Builder Annotation.

@Builder
public class Person {


public static class PersonBuilder {
private String name;


private PersonBuilder() {
}


public PersonBuilder(final String name) {
this.name = name;
}
}


private static PersonBuilder builder() {
return null; // or we can throw exception.
}


public static PersonBuilder builder(final String name) {
return new PersonBuilder(clientId);
}
}

Take User class as example, id field is required:

@AllArgsConstructor(access = AccessLevel.PRIVATE) // required, see https://stackoverflow.com/questions/51122400/why-is-lombok-builder-not-compatible-with-this-constructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Getter
public class User {
private String id;
private String name;
private int age;


public static UserBuilder builder(final String id) {
return new UserBuilder().id(id);
}
}

You can only initialize a User instance by builder like User user = User.builder("id-123").name("Tom").build;. With private no args constructer, you are not able to User user = new User(); or User user = new User("id-123"); so you always need to pass the required parameter id. Please note the initialized instance is immutable.

Combining the answer from @Pawel and comment from Max ...

import lombok.Builder;
import lombok.ToString;


@Builder
public class Person {


private String name;
private String surname;


public static PersonBuilder builder(String name) {
return new PersonBuilder().name(name);
}
}

Best Practice:

import lombok.Builder;
import lombok.NonNull;


@Builder(builderMethodName = "privateBuilder")
public class Person {
@NonNull
private String name;
private String surname;


public static class PersonNameBuilder {
public PersonBuilder name(String name) {
return Person.privateBuilder().name(status);
}
}


public static PersonNameBuilder builder(String name) {
return new PersonNameBuilder();
}


private static PersonBuilder privateBuilder(){
return new PersonBuilder();
}
}

Usage:

PersonNameBuilder nameBuilder = Person.builder();
PersonBuilder builder = nameBuilder.name("John");
Person p1 = builder.surname("Smith").build();


// Or
Person p2 = Person.builder().name("John").surname("Smith").build();

As much as I would like to have the compile time validation feature, the authors of the library had made it clear that the feature probably won't be added.

So my take on this is, to have something like this.

@Builder
public class Person {
String name;
Integer age;
Optional optional;


@Builder
public class Optional {
String surname;
String companyName;
String spouseName;
}

}

And you can use it like

 Person p = Person.builder()
.age(40)
.name("David")
.optional(Person.Optional.builder()
.surname("Lee")
.companyName("Super Company")
.spouseName("Emma")
.build())
.build();

No, there's no validation. But from the library's users point of view, it's pretty clear what's required and what's not and be able to build an object instance without looking at the documentation.

Here is an inspiration of Pawel response, with an hidden generated builder :

import lombok.Builder;
import lombok.ToString;


@Builder(builderMethodName = "")
@ToString
public class Person {


private String name;
private String surname;


public static PersonBuilder builder(String name) {
return new PersonBuilder().name(name);
}
}

In order to make limitations and risks implied by lombok's builder implementation as obvious as possible and therefore reduce the probability of erroneous abuse, I propose the following solution:

import lombok.Builder;


@Builder(builderClassName = "UnsafeBuilder")
public class Person {
private String name;
private String surname;


public static UnsafeBuilder builder(String name) {
return new UnsafeBuilder().name(name);
}
}

Using this solution as intended is straightforward:

Person thePerson = Person.builder("the_name").surname("the_surname").build();

The only way to use the builder in an unintended way makes the risk obvious, and most probably won't be chosen by mistake:

final Person unsafePerson = new Person.UnsafeBuilder().surname("the_surname").build();

The class name could of course be chosen even more radically - for example NeverCallThisConstructor.