Подтвердить что ты не робот

Как улучшить шаблон строителя?

Мотивация

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

Традиционный шаблон построителя

Когда я использую шаблон построителя для создания моего объекта Complex, создание более "типично", потому что легче увидеть, для чего используется аргумент:

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();

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

Улучшенный шаблон построителя

Теперь моя идея состояла в том, чтобы создать строителя, который "напоминает" мне, если я пропустил необходимый параметр. Моя первая попытка выглядит следующим образом:

public class Complex {
    private String m_first;
    private String m_second;
    private String m_third;

    private Complex() {}

    public static class ComplexBuilder {
        private Complex m_complex;

        public ComplexBuilder() {
            m_complex = new Complex();
        }

        public Builder2 setFirst( String first ) {
            m_complex.m_first = first;
            return new Builder2();
        }

        public class Builder2 {
            private Builder2() {}
            Builder3 setSecond( String second ) {
                m_complex.m_second = second;
                return new Builder3();
            }
        }

        public class Builder3 {
            private Builder3() {}
            Builder4 setThird( String third ) {
                m_complex.m_third = third;
                return new Builder4();
            }
        }

        public class Builder4 {
            private Builder4() {}
            Complex build() {
                return m_complex;
            }
        }
    }
}

Как вы можете видеть, каждый установщик класса строителя возвращает другой класс внутреннего строителя. Каждый внутренний класс строителя предоставляет ровно один метод setter, а последний - только метод build().

Теперь конструкция объекта будет выглядеть следующим образом:

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();

... но нет способа забыть необходимый параметр. Компилятор не согласился с этим.

Необязательные параметры

Если бы у меня были необязательные параметры, я бы использовал последний класс встроенного builder Builder4, чтобы установить их как "традиционный" построитель, возвращаясь.

Вопросы

  • Это хорошо известный шаблон? У него есть специальное имя?
  • Вы видите какие-то подводные камни?
  • Есть ли у вас идеи улучшить реализацию - в смысле меньшего количества строк кода?
4b9b3361

Ответ 1

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

Лично я считаю, что это отличное расширение шаблона построителя, и вы можете делать с ним всевозможные интересные вещи, например, на работе у нас есть сборщики DSL для некоторых наших тестов целостности данных, которые позволяют нам делать такие вещи, как assertMachine().usesElectricity().and().makesGrindingNoises().whenTurnedOn();. Хорошо, может быть, не самый лучший пример, но я думаю, что вы поняли.

Ответ 2

Традиционный шаблон строителя уже справляется с этим: просто возьмите обязательные параметры в конструкторе. Конечно, ничто не мешает звонящему передать значение null, но и ваш метод.

Большая проблема, которую я вижу в вашем методе, заключается в том, что вы либо комбинаторный взрыв классов с количеством обязательных параметров, либо заставляете пользователя задавать параметры в одной конкретной последовательности, что раздражает.

Кроме того, много работы.

Ответ 3

Почему бы вам не поставить "нужные" параметры в конструкторе строителей?

public class Complex
{
....
  public static class ComplexBuilder
  {
     // Required parameters
     private final int required;

     // Optional parameters
     private int optional = 0;

     public ComplexBuilder( int required )
     {
        this.required = required;
     } 

     public Builder setOptional(int optional)
     {
        this.optional = optional;
     }
  }
...
}

Этот шаблон описан в Эффективная Java.

Ответ 4

public class Complex {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public Complex(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}

Ответ 5

ИМХО, это кажется раздутым. Если у вас есть, чтобы иметь все параметры, передайте их в конструкторе.

Ответ 6

Вместо использования нескольких классов я бы просто использовал один класс и несколько интерфейсов. Он применяет ваш синтаксис, не требуя такого набора текста. Это также позволяет вам видеть все связанные коды близко друг к другу, что облегчает понимание того, что происходит с вашим кодом на более высоком уровне.

Ответ 7

Я видел/использовал это:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();

Затем передайте их вашему объекту, который их требует.

Ответ 8

Шаблон Builder обычно используется, когда у вас есть множество необязательных параметров. Если вы обнаружите, что вам нужно множество требуемых параметров, сначала рассмотрите эти параметры:

  • Ваш класс может делать слишком много. Двойная проверка, что она не нарушает принцип единой ответственности. Спросите себя, почему вам нужен класс с таким количеством необходимых переменных экземпляра.
  • Вы можете создать сделать слишком много. Задача конструктора - построить. (Они не очень креативны, когда их называют, D). Подобно классам, методы имеют принцип единой ответственности. Если ваш конструктор делает больше, чем просто назначение поля, вам нужна веская причина для его оправдания. Возможно, вам понадобится Factory Method, а не Builder.
  • Ваши параметры могут быть делать слишком мало. Спросите себя, могут ли ваши параметры быть сгруппированы в небольшой struct (или структурно подобный объект в случае Java). Не бойтесь делать небольшие занятия. Если вы обнаружите, что вам нужно создать структуру или небольшой класс, не забывайте рефакторинг out функциональность, которая принадлежит struct, а не ваш более крупный класс.

Ответ 9

Для получения дополнительной информации о , когда использовать шаблон Builder и его преимущества, вы должны проверить мой пост для другого аналогичного вопроса здесь

Ответ 10

Вопрос 1: Что касается имени шаблона, мне нравится имя "Step Builder":

Вопрос 2/3: Что касается подводных камней и рекомендаций, в большинстве ситуаций это усложняется.

  • Вы выполняете последовательность в том, как вы используете свой строитель, что необычно для моего опыта. Я мог видеть, как это было бы важно в некоторых случаях, но я никогда не нуждался в нем. Например, я не вижу необходимости форсировать последовательность здесь:

    Person.builder().firstName("John").lastName("Doe").build() Person.builder().lastName("Doe").firstName("John").build()

  • Однако много раз строителю требовалось принудительное выполнение некоторых ограничений для предотвращения создания фальшивых объектов. Возможно, вы хотите обеспечить наличие всех обязательных полей или допустимых комбинаций полей. Я предполагаю, что это настоящая причина, по которой вы хотите ввести секвенирование в здание.

    В этом случае мне нравится рекомендация Джошуа Блоха выполнить валидацию в методе build(). Это помогает с проверкой поперечного поля, потому что все доступно в этот момент. См. Этот ответ: https://softwareengineering.stackexchange.com/a/241320

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