Как улучшить шаблон строителя? - PullRequest
39 голосов
/ 28 октября 2009

Мотивация

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

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

Когда я использую шаблон построителя для создания моего 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;
            }
        }
    }
}

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

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

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

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

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

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

Вопросы

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

Ответы [ 10 ]

23 голосов
/ 28 октября 2009

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

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

Также, много дополнительной работы.

15 голосов
/ 28 октября 2009

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

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

13 голосов
/ 27 июля 2013
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"));
    }
}
12 голосов
/ 28 октября 2009

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

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

Этот шаблон описан в Effective Java .

7 голосов
/ 07 июля 2011

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

5 голосов
/ 28 октября 2009

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

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

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

5 голосов
/ 28 октября 2009

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

2 голосов
/ 23 февраля 2013

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

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

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

0 голосов
/ 05 марта 2015

Вопрос 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

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

...