Шаблон проектирования Fluent Builder - как заставить клиента устанавливать переменные

У меня проблема с шаблоном проектирования. Скажем, у меня есть простой класс:

public class Person {

    private String name;
    private String surname;
    private int age;

    private Person(){}

    public String toString()
    {
        return "name: "+name+" surname: "+surname+" age: "+age;
    }

    public static final class Builder{

        private String name;
        private String surname;
        private int age;

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

        public Builder surname (String surname){
            this.surname = surname;
            return this;
        }

        public Builder age (int age){
            this.age = age;
            return this;
        }

        public Person build(){
            Person person = new Person();
            person.name=name;
            person.surname=surname;
            person.age=age;
            return person;
        }
    }
}

Он работает нормально, но не требуется устанавливать переменные перед вызовом метода "build". Как я мог это изменить?

Вы можете использовать построитель на основе лямбда для принудительного выполнения последовательности вызовов методов, прежде чем пользователь сможет вызвать build().

Boris the Spider 30.03.2021 00:51

@BoristheSpider, существует так много разных версий паттерна Builder. Не могли бы вы дать ссылку на тот, о котором вы говорите?

jaco0646 30.03.2021 02:01
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
2
23
2

Ответы 2

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

Альтернативой, если вы согласны с соблюдением некоторого порядка, может быть использование объектов "step":

class Builder {
    private String name;
    private String surname;
    private int age;

    //start with the name and create a new builder
    public static SurnameStep name(String name) {
        Builder builder = new Builder();
        builder.name = name;
    
        return builder.new SurnameStep();       
    }

    public class SurnameStep {
        public AgeStep surname(String surname) {
            Builder.this.surname = surname;
            return new AgeStep();
        }
    }

    public class AgeStep {
        public Builder age(int age) {
            Builder.this.age = age;
            return Builder.this;
        }
    }

    public Person build() {
        //build person here
    }
}

Тогда использование должно быть таким:

Person p = Builder.name("John").surname("Smith").age(42).build();

Имя, фамилия и возраст должны быть указаны в указанном порядке, прежде чем build() станет доступен.

Если все поля являются обязательными, вы также можете избавиться от build() и создать Person уже в age(int).

Также обратите внимание, что вместо того, чтобы помещать шаги в Builder, вы можете поместить их прямо в Person. Статический метод "init" name(String) уже создал бы экземпляр Person, но он станет доступным только после вызова age(int):

//expanded to make it more obvious: the steps don't provide access to the Person until all have been executed
SurnameStep surnameStep = Person.name("John");
AgeStep ageStep = surnameStep .surname("Smith");
Person johnSmith = ageStep.age(42);

Вы можете улучшить этот код, выполнив следующие действия:

  1. Создайте нестандартный конструктор Person, который принимает все 3 значения
public class Person {
    public Person(String first, String last, int age) {
        this.first = first;
        this.last = last;
        this.age = age;
    }
}
  1. Добавьте в этот конструктор проверку аргументов, выбрасывая исключение IllegalArgumentException
if (age < 0) {
    throw new IllegalArgumentException("Age cannot be negative: " + age);
}
if (first == null && last == null) {
    throw new IllegalArgumentException("first or last name is null");
}
  1. (Необязательно, для использования в модульных тестах) вы также можете инициализировать значения по умолчанию в Builder для целей тестирования в статическом заводском методе и конструкторе не по умолчанию:
public static Builder someAdultPerson() {
    return new Builder("Erika", "Mustermann", 18);
}
  1. Поддерживайте нотацию BDD в ваших тестах с помощью этого интерфейса:
interface Builder<T> {
    T build();
    static <C> given(Builder<C> builder) { return builder.build(); }
}
... 
class Person {
    class Builder implements my.example.Builder<Person> { ... }
}
...
import static Person.Builder.someAdultPerson;
...
Person person = given(someAdultPerson().age(27));

Другие вопросы по теме