Обязательные аргументы с Lombok @Builder

Если я добавлю @Builder в класс. Метод построителя создан.

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

У меня есть требование, где требуется конкретное поле. В этом случае поле имени обязательно, а фамилия нет. В идеале я хотел бы объявить это так.

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

Я не могу понять, как это сделать. Я попытался добавить @Builder в конструктор, но это не сработало.

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

person jax    schedule 27.04.2015    source источник
comment
Открытые проблемы Lombok на GitHub имеют одну открытую проблему для этого github.com/rzwitserloot/lombok/issues/1043   -  person lennykey    schedule 14.02.2018
comment
Вероятно, это не будет работать с ломбоком, так как вам нужен доступ к фактическому исходному коду сборщика, но если у вас есть доступ, вы можете попробовать плагин, который я разработал для решения именно таких проблем: github.com/banterly91/   -  person dragosb    schedule 26.01.2021


Ответы (11)


Вы можете легко сделать это с помощью конфигурации аннотаций Lombok.

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

А потом использовать так

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

Конечно, @ToString здесь необязателен.

person Pawel    schedule 16.06.2015
comment
было бы лучше, если бы вы могли объяснить немного больше. - person Blip; 16.06.2015
comment
@Blip означает, что генератор создаст метод с именем hiddenBuilder вместо builder. Затем пользовательский метод builder вызывает его с необходимыми аргументами (которые могут быть любыми по вашему желанию). - person jax; 16.06.2015
comment
@Blip, как объяснил jax, builderMethodName value - это имя метода, который будет создан автоматически. Запрос состоял в том, чтобы иметь метод builder с параметром String name, поэтому понадобилось другое имя автоматически сгенерированного метода компоновщика. Также вы должны прочитать документацию lombok @Builder, чтобы понять, что lombok также генерирует PersonBuilder класс для нас. - person Pawel; 19.06.2015
comment
что для меня не имеет смысла в этом ответе, так это то, что hiddenBuilder() не скрыт... - person Kevin Day; 17.03.2016
comment
@KevinDay скрыто от посторонней точки зрения. Извне вы просто вызываете builder(), и эта функция вызовет hiddenBuilder() - person Akshat Agarwal; 13.10.2016
comment
@AkshatAgarwal, что делает метод Builder скрытым? Я на 99,99% уверен, что builderMethodName просто меняет имя метода, а не делает его скрытым. Поэтому я до сих пор не вижу способа добиться желаемого результата наличия обязательных полей. - person Kevin Day; 09.11.2016
comment
@KevinDay, ты прав, он этого не скрывает. Извините за неверную информацию - person Akshat Agarwal; 09.11.2016
comment
Извините, builder() все еще виден! - person emeraldhieu; 03.01.2017
comment
Так в Ломбоке нельзя добиться такого эффекта, чтобы создать билдер с аргументом? - person Kamil Tomasz Jarmusik; 06.08.2018
comment
добавив к этому, чтобы сделать hiddenBuilder скрытым по-настоящему, вы можете добавить следующий код в класс private PersonBuilder hiddenBuilder() { return new PersonBuilder(); } - person Max; 29.04.2019
comment
Я бы просто сказал Ломбоку сделать конструктор приватным: @Builder(builderMethodName = "hiddenBuilder", access = AccessLevel.PRIVATE) - person Linus; 22.10.2019
comment
Просто добавьте к комментарию @Linus, что для установки доступа вам нужен как минимум Lombok v1.18.8. - person labm0nkey; 12.11.2019
comment
@Linus кажется, что добавление AccessLevel.PRIVATE также делает все методы построителя закрытыми и считает их совершенно бесполезными. Я ошибаюсь? - person Dean Gurvitz; 26.01.2020
comment
@DeanGurvitz Я только что проверил версию 1.18.10, все методы сборщика остаются общедоступными. - person Linus; 28.01.2020
comment
@DeanGurvitz Забыл обновить плагин Lombok для IntelliJ Idea. Вы правы, все методы приватные, так что такой подход не работает. - person Linus; 29.01.2020
comment
Начиная с версии lombok v1.18.8, вы можете сделать @Builder(builderMethodName = ""), в результате чего lombok теперь будет создавать для вас любой метод сборки. Затем вы можете создать его самостоятельно, как показано в этом ответе. - person scottg489; 25.11.2020

Я бы не рекомендовал этот подход, так как вам будет сложно последовательно применять его к другим объектам. Вместо этого вы можете просто пометить поля аннотацией @lombok.NonNull, и Lombok сгенерирует для вас нулевые проверки в конструкторе и сеттерах, так что Builder.build() завершится ошибкой, если эти поля не установлены.

Использование шаблона построителя позволяет вам очень четко определить, какие поля вы устанавливаете для каких значений. Это уже потеряно для поля имени в вашем примере, и оно будет потеряно для всех других обязательных полей, если вы создаете объект с несколькими обязательными полями. Рассмотрим следующий пример. Можете ли вы определить, какое поле какое, просто прочитав код?

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()
person Anton Koscejev    schedule 30.03.2016
comment
Ошибка времени выполнения и ошибка времени компиляции. Всегда отдавайте предпочтение ошибке времени компиляции! - person jax; 31.03.2016
comment
@jax Они не проверяют одно и то же. Требование установки поля не проверяет нулевое значение. Проверка на нуль будет ошибкой времени выполнения (в чистой Java) независимо от того, требуется ли вам это поле или нет. - person Anton Koscejev; 01.04.2016
comment
Нет, метод построителя не может быть запущен до тех пор, пока не будут переданы все необходимые аргументы, что соответствует контракту классов. Ваш способ позволяет создать недопустимый объект во время выполнения. - person jax; 01.04.2016
comment
Теперь я понимаю, что вы имеете в виду, но ваш метод также допускает нулевые значения. Лучше быть открытым с контрактом объекта, чем заставлять программиста гадать. Взгляните на шаблон построителя в книге под названием Effective Java 2nd edition. - person jax; 01.04.2016
comment
Builder не может создать недопустимый объект во время выполнения, так как он использует конструктор со всеми аргументами, созданный Lombok. Этот конструктор выполняет нулевые проверки для параметров, аннотированных с помощью @NonNull. Сюда входят параметры, которые вы никогда не указывали явно с помощью методов компоновщика. - person Anton Koscejev; 03.04.2016
comment
@NotNull будет оцениваться во время выполнения! - person jax; 03.04.2016
comment
Вы также можете создавать нулевые поля с предыдущими ответами, например, Person.builder(null).lastName(John).build(); поэтому вам все равно понадобится проверка во время выполнения, несмотря ни на что. - person Lakatos Gyula; 11.05.2018
comment
@LakatosGyula, я согласен, но я думаю, что по соглашению у разработчиков меньше шансов вызвать обязательные поля с нулевым значением, у вас, вероятно, будет больше ошибок при использовании обычного компоновщика, это компромисс, и я бы пошел с ответом Антона . - person David Barda; 11.04.2020
comment
@DavidBarda Единственный способ, с помощью которого пользовательский метод построителя уменьшает количество ошибок, - это заставить вызывающую сторону передать значение, например. они не могут пропустить вызов обязательного метода установки. Несмотря на это, ему все равно понадобятся нулевые проверки. Задание констант в построителе — очень редкий вариант использования за пределами классной комнаты. Я не думаю, что стоит торговать согласованностью и читабельностью. - person Torben; 11.05.2021

Сделав ответ Кевина Дэя еще на один шаг:

@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);
    }

}

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

person The Gilbert Arenas Dagger    schedule 28.03.2018
comment
хорошо, но по-прежнему можно переопределить значение (по ошибке), вызвав .name(), чтобы мы получили: Person.builder("john").surname("smith").name("mark").build(); и, конечно, в конечном итоге мы получили бы mark нежелательное john. Я знаю, что это немного надуманный случай, но возможный, и идеальное решение позволит избежать этой ситуации. Как вы думаете, можно ли от этого защититься? (сейчас занимаюсь этим) - person user3529850; 27.10.2018
comment
@user3529850 user3529850, если у вас есть обязательные поля И по какой-то причине проблемы с неизменностью в контексте вашего компоновщика, тогда вам лучше просто написать свой настраиваемый компоновщик по старинке, без Ломбока. Обратите внимание, что в примере, который я привел, Person IS является неизменным. Вы можете задать имя ровно один раз. Что вы делаете, так это устанавливаете имя несколько раз в PersonBuilder, которое не должно быть неизменным. - person The Gilbert Arenas Dagger; 28.10.2018
comment
@user3529850 user3529850 Даже при использовании классического конструктора вы всегда можете передать значение null в конструкторе... - person Amit Goldstein; 16.06.2019

Вот еще один подход:

@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();
    }
}
person Kevin Day    schedule 17.03.2016
comment
Я предпочитаю ваш подход, так как он просто перегружает метод builder, делая синтаксис лаконичным и естественным. - person Jezor; 03.09.2016
comment
Проблема с этим подходом заключается в том, что builder() все еще виден, поэтому он на самом деле не создает требуемые параметры. Во что бы то ни стало, да, нам также нужно использовать аннотацию @NonNull, но это проверка во время выполнения — определенно недостаточно, если кто-то пытается создать суперинтуитивный объект. Жаль, что в ломбоке нет способа адаптировать такие вещи — даже если бы мы могли сделать метод builder() приватным, мы могли бы затем создать наш собственный перегруженный общедоступный метод builder(...) с необходимыми параметрами. - person Kevin Day; 09.11.2016
comment
Очевидно, за последние пять лет что-то изменилось, поэтому с таким подходом builder() больше не будет доступен. В отличие от принятого anwear, это также не дает мне нескрытый hiddenBuilder(), что делает этот подход моим любимым. - person Tobias Grunwald; 19.04.2021
comment
Спасибо за внимание @TobiasGrunwald - это делает мой подход почти идеальным (хотя это вызывает у меня небольшое беспокойство по поводу обратной совместимости для тех, кто все еще обращался к методу построения без аргументов!) - person Kevin Day; 20.04.2021

Самое простое решение — добавить @lombok.NonNull ко всем обязательным значениям. Построитель не сможет построить объект, если обязательные поля не установлены.

Вот тест JUnit, демонстрирующий поведение всех комбинаций final и @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;
    }
}
person lilalinux    schedule 02.03.2018

Это мое решение проблемы

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

вдохновленный этой записью в блоге:

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

person kozla13    schedule 15.12.2016
comment
Это то, что я хочу, чтобы ломбок сгенерировал для меня. - person okutane; 23.05.2017
comment
@okutane Похоже, этого не происходит: groups.google.com/d/ msg/project-lombok/gjUAHljdSK0/CdizSESdEAAJ - person Nick; 16.08.2020

В качестве примера возьмем класс User, поле id обязательно:

@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);
    }
}

Вы можете только инициализировать экземпляр User построителем, например User user = User.builder("id-123").name("Tom").build;. С частным конструктором без аргументов вы не можете User user = new User(); или User user = new User("id-123");, поэтому вам всегда нужно передавать требуемый параметр id. Обратите внимание, что инициализированный экземпляр неизменяем.

person coderz    schedule 13.09.2019
comment
Но я предполагаю, что вы все еще можете вызывать конструктор UserBuilder по умолчанию без аргументов, никто его не останавливает. - person Shadman R; 14.07.2021

Объединение ответа от @Pawel и комментария Макса...

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);
  }
}
person codemano    schedule 20.09.2019
comment
Имейте в виду, что это, к сожалению, не работает с @SuperBuilder, поскольку сгенерированный класс строителя также является абстрактным. - person Marv; 05.12.2020

Лучшая практика:

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

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

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();
person dallaslu    schedule 25.05.2021

Если вам нужна эта функциональность, вы можете самостоятельно настроить класс построителя и добавить @Builder аннотацию.

@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);
    }
}
person Krishna M    schedule 26.06.2019

Как бы мне ни хотелось иметь функцию проверки времени компиляции, авторы библиотеки ясно дали понять, что функция, вероятно, не будет добавлена.

Так что мой взгляд на это, чтобы иметь что-то вроде этого.

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

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

}

И вы можете использовать его как

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

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

person iecanfly    schedule 31.07.2021