Пользователь
0,0
рейтинг
3 марта 2010 в 20:23

Разработка → Использование паттерна Builder в случае, когда мы сталкиваемся с конструктором с многими параметрами

Java*
Статья представляет вольный перевод главы из книги Effective Java, Second Edition by Joshua Bloch

В статье рассматриваются 3 альтернативных подхода к упрощению использования класса, с конструктором с многими параметрами.


Рассмотрим случай, когда у нас есть класс, предоставляющий надпись на упаковке продуктов питания. Данная надпись отвечает за химический состав продукта. Эта надпись имеет несколько обязательных полей: размер порции, количество порций в контейнере, калорийность одной порции и около двадцати дополнительных полей: всего жиров, насыщенных жиров, транс-жиров, количество холестерина, количество натрия, и так далее. Большинство продуктов имеют ненулевые значения лишь для немногих из этих дополнительных полей.


Первая альтернатива (Telescoping Constructor паттерн)



Традиционно, программисты использовали Telescoping Constructor паттерн. Суть этого паттерна состоит в том, что Вы предоставляете несколько конструкторов: конструктор с обязательными параметрами, конструктор с одним дополнительным параметром, конструктор с двумя дополнительными параметрами, и так далее. Продемонстрируем как это будет выглядеть на практике. Для краткости будем использовать только 4 дополнительных параметра.


// Telescoping constructor pattern - плохо масштабируемый!
public class NutritionFacts {

  private final int servingSize;   // обязательный параметр
  private final int servings;   // обязательный параметр
  private final int calories;   // дополнительный параметр
  private final int fat;       // дополнительный параметр
  private final int sodium;     // дополнительный параметр
  private final int carbohydrate; // дополнительный параметр

  public NutritionFacts(int servingSize, int servings) {
    this(servingSize, servings, 0);
  }

  public NutritionFacts(int servingSize, int servings, int calories) {
    this(servingSize, servings, calories, 0);
  }

  public NutritionFacts(int servingSize, int servings, int calories, int fat) {
    this(servingSize, servings, calories, fat, 0);
  }

  public NutritionFacts(int servingSize, int servings, int calories, int fat,
      int sodium) {
    this(servingSize, servings, calories, fat, sodium, 0);
  }

  public NutritionFacts(int servingSize, int servings, int calories, int fat,
      int sodium, int carbohydrate) {
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = calories;
    this.fat = fat;
    this.sodium = sodium;
    this.carbohydrate = carbohydrate;
  }
}




Когда Вы хотите создать объект данного класса, Вы используете конструктор с необходимым списком параметров:


NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);



Обычно для вызова конструктора потребуется передавать множество параметров, которые Вы не хотите устанавливать, но Вы в любом случае вынуждены передать для них значение. В нашем случае, мы установили значение 0 для поля fat. Поскольку мы имеем только шесть параметров, может показаться, что это не так уж и плохо. Но это начинает доставлять огромные проблемы когда число параметров увеличивается.


Короче говоря, используя Telescoping Constructor паттерн, становится трудно писать код клиента, когда имеется много параметров, а еще труднее этот код читать. Читателю остается только гадать, что означают все эти значения и нужно тщательно высчитывать позицию параметра, чтобы выяснить к какому полю он относится. Длинные последовательности одинаково типизированных параметров могут причинить тонкие ошибки. Если клиент случайно перепутает два из таких параметров, то компиляция будет успешной, но программа будет работать не верно.


Вторая альтернатива (JavaBeans паттерн)



Второй вариант, когда Вы столкнулись с конструктором с многими параметрами — это JavaBeans паттерн. Вы вызываете конструктор без параметров, чтобы создать объект, а затем вызываете сеттеры для установки обязательных и дополнительных параметров, представляющих интерес:


// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
  // Параметры инициализируются значениями по умолчанию
  private int servingSize = -1;   // Обязательный
  private int servings = -1;     // Обязательный
  private int calories = 0;
  private int fat = 0;
  private int sodium = 0;
  private int carbohydrate = 0;

  public NutritionFacts() {
  }

  // Сеттеры
  public void setServingSize(int val) {
    servingSize = val;
  }

  public void setServings(int val) {
    servings = val;
  }

  public void setCalories(int val) {
    calories = val;
  }

  public void setFat(int val) {
    fat = val;
  }

  public void setSodium(int val) {
    sodium = val;
  }

  public void setCarbohydrate(int val) {
    carbohydrate = val;
  }
}




Данный подход лишен недостатков Telescoping Constructor паттерна (Объект легко создавать и полученный код легко читать):


NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);




К сожалению, JavaBeans паттерн не лишен серьезных недостатков. Поскольку строительство разделено между несколькими вызовами, JavaBean может находиться в неустойчивом состоянии частично пройдя через конструирование. Попытка использования объекта, если он находится в неустойчивом состоянии может привести к ошибкам, которые далеки от кода, содержащего ошибку, следовательно, трудными для отладки. Также JavaBeans паттерн исключает возможность сделать класс неизменным(immutable), что требует дополнительных усилий со стороны программиста для обеспечения безопасности в многопоточной среде.


Третья альтернатива (Builder паттерн)



К счастью, есть и третья альтернатива, которая сочетает в себе безопасность паттерна Telescoping Constructor с читаемостью паттерна JavaBeans. Она является одной из форм паттерна Builder(Строитель). Вместо непосредственного создания желаемого объекта, клиент вызывает конструктор (или статическую фабрику) со всеми необходимыми параметрами и получает объект строителя. Затем клиент вызывает сеттер-подобные методы у объекта строителя для установки каждого дополнительного параметра. Наконец, клиент вызывает метод build() для генерации объекта, который будет являться неизменным(immutable). Строитель является статическим внутренним классом в классе, который он строит. Вот как это выглядит на практике:


// паттерн Builder
public class NutritionFacts {
  private final int servingSize;
  private final int servings;
  private final int calories;
  private final int fat;
  private final int sodium;
  private final int carbohydrate;

  public static class Builder {
    // Обязательные параметры
    private final int servingSize;
    private final int servings;
    // Дополнительные параметры - инициализируются значениями по умолчанию
    private int calories = 0;
    private int fat = 0;
    private int carbohydrate = 0;
    private int sodium = 0;

    public Builder(int servingSize, int servings) {
      this.servingSize = servingSize;
      this.servings = servings;
    }

    public Builder calories(int val) {
      calories = val;
      return this;
    }

    public Builder fat(int val) {
      fat = val;
      return this;
    }

    public Builder carbohydrate(int val) {
      carbohydrate = val;
      return this;
    }

    public Builder sodium(int val) {
      sodium = val;
      return this;
    }

    public NutritionFacts build() {
      return new NutritionFacts(this);
    }
  }

  private NutritionFacts(Builder builder) {
    servingSize = builder.servingSize;
    servings = builder.servings;
    calories = builder.calories;
    fat = builder.fat;
    sodium = builder.sodium;
    carbohydrate = builder.carbohydrate;
  }
}



Обратите внимание, что NutritionFacts является неизменным(immutable), и что все значения параметров по умолчанию находятся в одном месте. Сеттер-методы строителя возвращают сам этот строитель. Поэтому вызовы можно объединять в цепочку. Вот как выглядит код клиента:


NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();


Этот клиентский код легко писать и, что еще важнее, легко читать. Паттерн Builder имитирует именные дополнительные параметры, которые используются в Ada и Python.


UPDATE
Согласно комментарию предлагается:
для рассматриваемого класса NutritionFacts логично предусмотреть getter'ы для неизменяемых полей. Иначе получается, что объект-то мы построим, а воспользоваться им не сможем.
Поля Builder'а логичнее именовать в соответствием с конвенцией JavaBeans, а именно setXXX(). Поскольку данный способ является уже стандартом де-факто для Java, подобный подход улучшит читабельность кода.

з.ы. Моя первая публикация на хабре. Сильно не пинайте ;)

feodal @feodal
карма
17,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (52)

  • +1
    Круто.
    • +2
      если данный мой перевод сделал что-то полезное хотя бы одному человеку, то время мной было потрачено не зря ;)
      • +1
        Было бы здорово читать переводы таких статей. Вроде простая вещь, и не понятно как работал без этого раньше. Спасибо большое. Продолжайте!
  • 0
    Да, такой код действительно и легко читается, но вот создавать билдера, точнее изменять, с изменениями основного класса будет уже лениво. А вообще похоже на монодинаический код.
    • +1
      при изменении основного класса все-равно надо будет добавлять или новый конструктор(при использовании Telescoping Constructor паттерн) или добавлять новый сеттер (при использовании JavaBeans паттерн). поэтому добавления нового метода в билдер и изменение конструктора основного класса не особо лениво будет выглядить.
  • НЛО прилетело и опубликовало эту надпись здесь
  • –1
    Если ваш класс требует большого кол-во аргументов в конструкторе, то это — сигнал к рефакторингу.
    • 0
      ну можно воспринимать данный подход как рефакторинг к шаблону Билдер
      • +1
        Этот шаблон не избавит нас от плохо спроектированного класса, а лишь скроет проблему.
        • +2
          А если у нас бин, отражающий некую сущность предметной области, с большим количеством атрибутов (как следствие класс ее моделирующий имеет множество полей), то что тогда делать? Как следует отрефакторить?
          • +2
            А ведь такое действительно бывает, хотя случай и немного экзотический. У нас в проекте есть классы с несколькими десятками полей. И не отделить никак.
          • 0
            Можно конкретный пример?
            • 0
              Легко, у нас, например, у User`a более десятка полей контактной информации (на этапе регистрации заполнить достаточно три — мыло, пароль, last name, это чтоб не пугать :)).

              Хотя, конечно, user, под данный шаблон совсем не подходит, но вы ведь просили пример сущности с большим кол-вом полей :)
              • 0
                Ну так можно все же пример, который подходит под данный шаблон? Безусловно, такие примеры найти можно. Вопрос в том, насколько это часто встречается.
                • +1
                  Пример:
                  есть вспомогательная форма с деревом объектов в нем. Входящие параметры на форму: параметры, по которым будет фильтроваться дерево. Как правило, их количество около 4-5. И в зависимости от них надо формировать запрос на построение дерева.
                  У нас в проекте таких много.

                  Буквально вчера в очередной раз создавал «телескоп» и тихо матерился про себя, что в двух вызовах приходится проставлять null вместо параметров «в середине».
                  • 0
                    Либо я не правильно вас понял, либо вопрос «что мешает использовать setXXX?» будет вполне уместным.

                    Лично для себя я вывел правило: если где-то приходится использовать null, то с большой степенью вероятности код написан плохо.
                    • 0
                      Плохо поняли. Не бину, а форме. Форма уже рулит произвольным количеством бинов, задавая входящие параметры для запросов для получения этих бинов.

                      Детализация примера: есть форма построения иерархии фирма — продукт. Есть два параметра: тип фирм, тип продуктов. В зависимости от контекста должны фильтроваться один из или обе сущности сразу. В зависимости от того, что из параметров не null (или не любой другой фейк), создается соответствующий whereClause. Если фейк — не фильтруем сущности.
                      Так понятнее?

                      Если правило работает для вас, рад за вас. =) Только не надо так явно спускать его мне. С тем же успехом можно сделать правило, что там, где 0 — плохо. Потому что деление на ноль.
                      • 0
                        В любом случае каждый инструмент предназначен для решения своей задачи. И если инструмент позволяет скрыть плохо спроектированный код за абстракцией, то это не значит что код становится лучше.

                        Если честно, все равно не понял. Предлагаю на этом закончить:)

                        ЗЫ: А правило это я для себя вывел, т.к. не очевидно является ли для метода параметр null допустимым или нет. Приходится писать документацию, которая отнюдь не обязательно избавит от ошибок. А раз что-то не очевидно из кода, то код плох. Примерно так.
                        • 0
                          У меня почему-то такое чувство, что вы и не хотели понимать. =)
                          Тем не менее, надеюсь, кому-нибудь еще это будет полезным.
                          Заканчиваем.
  • +1
    Вах, век живи — век учись. Красивый паттерн.
  • 0
    Интересная идея. Как раз думаю как мне лучше передать кучу параметров в граф.

    Но в чем плюс перед следующим кодом:
    // Telescoping constructor pattern — плохо масштабируемый!
    public class NutritionFacts {

    private final int servingSize; // обязательный параметр
    private final int servings; // обязательный параметр
    private final int calories; // дополнительный параметр
    private final int fat; // дополнительный параметр
    private final int sodium; // дополнительный параметр
    private final int carbohydrate; // дополнительный параметр

    public NutritionFacts(int servingSize, int servings) {
    this.servingSize = servingSize;
    this.servings = servings;
    }
    //+сеттеры
    }



    • 0
      блин, отправилось раньше времени.
      используем этот код тогда так:
      NutritionFacts cocaCola = new NutritionFacts(240, 8);
      остальные параметры выставляем сеттерами.

      Выйгрышь получается только в синтаксисе? Зато не надо класс дополнительный создавать.
      • 0
        Выигрыш в ленивой инициализации класса. То есть можно иметь как бы частично инициализированный класс и из него (типа-шаблона) создавать уже готовые инстансы.
        Нужно, например, когда в цикле выдаём почти одинаковые объекты.
    • 0
      прочитайте в статье про недостатки JavaBeans паттерна
      • 0
        А. Я несколько невнимательно отнесся к тому когда вызывается build у паттерна builder. Тогда да — вопрос снят.
    • +1
      Но в чем плюс перед следующим кодом:

      Нууу, как бы вам сказать… Примерно в том, что ваш код не скомпилится). У вас дополнительные параметры объявлены как final и не сетятся в конструкторе. Компилятор ругнется на variable might not have been initialized.
      • +1
        я просто не пишу на java. Что я имел ввиду в коде я думаю понятно)
  • 0
    Действительно красивое решение, но вот думаю на сколько будет дружить например с Hibernate, который ожидает POJO? хотя все get/set объявить private, вроде должно работать
    • +1
      Ну для hibernate можно просто аннотации на поля вешать, да и POJO это не есть JavaBean, это просто объект, данный термин появился во время массового сруливания с EJB 2х. Где вместо текущих POJO были так называемые EntityBean's.
      • 0
        А в случае с аннотациями наличие get/set не обязательно? (у меня почему то отложилось в памяти, что они все таки нужны).
        • –1
          Сдесь в прицыпе не важно с аннотациями или без, я просто с XML со 2-го гибера не работал, но и там можно было определить доступ default-access=«field|property». Просто когда на аннотациях делается маппинг, если аннотация PK'ея ставится на поле, тогда гибер будет работать не через методы, а обращатся к полям. Для обратного эффекта ставим аннотацию на метод.
  • –1
    Если мне не изменяет память в нотации GoF данный паттерн называется Composite en.wikipedia.org/wiki/Composite_pattern, а Builder это совсем другой паттерн. Да кстати и ссылку вы даете на описание Builder'а так как он описан у GoF. Поправте если не прав.
    • +1
      Builder это именно тот паттерн, что используется (здесь можно почитать на русском codelab.ru/pattern/builder/).
      ГоФ про билдер пишет «отделяет конструирование сложного объекта от его представления, так что в результате одного и того же процесса конструирования могут получаться различные представления»

      Паттерн композитор «компонует объекты в древовидные структуры для представления иерархий часть-целое. Позволяет клиентам единообразно трактовать индивидуальные и составные объекты» (ГоФ)
      • –1
        Ну вот не builder здесь описан, ну не похож. Самое интересное, что ссылки все дают на верное описание. Отделением создания объекта от его представления занимаются все порождающие паттерны.
  • +2
    см. также «Fluent Interfaces»: en.wikipedia.org/wiki/Fluent_interface
    • +1
      Спасиб, теперь я знаю грамотное название паттерна на который я подумал. :)
  • +1
    А нельзя просто из всех сеттеров возвращять this?
    • +1
      Можно, но во-первых это не хорошо, когда сеттер что-то возвращает — он же сеттер. Во-вторых это не делает объект не изменяемым, в отличии от билдера.
  • +1
    Как раз начал читать эту книгу на английском. Перевод классный, как в прочем и книга. Однозначно рекомендую к прочтению целиком.
    • 0
      спасибо, что оценили перевод, старался.

      а книга действительно must read, но только для практика, а не для человека только изучающего java.
  • +1
    Как альтернативу можно предложить передавать в конструктор HashMap с параметрами, а уже внутри него вызывать get/set'еры. Обязательные параметры в этом случае можно передавать отдельно от HashMap (если их мало), либо вместе с HashMap и мониторить их наличие в конструкторе (при отсутствии кидать Exception).
    • 0
      Проблема в том что HashMap не говорит какие свойства нужно про инициализировать в нем что бы корректно создать объект. Поэтому приходится подробно описывать в документации структуру HashMap.
      • 0
        Документировать имхо нужно в любом случае всё и вся.
  • +1
    Предлагаю улучшения к статье, чтобы пример стал более жизненным:
    • Для рассматриваемого класса NutritionFacts логично предусмотреть getter'ы для неизменяемых полей. Иначе получается, что объект-то мы построим, а воспользоваться им не сможем.
    • Поля Builder'а логичнее именовать в соответствием с конвенцией JavaBeans, а именно setXXX(). Поскольку данный способ является уже стандартом де-факто для Java, подобный подход улучшит читабельность кода.
    • 0
      согласен, спасибо за замечание
    • +1
      setXXX() — это стандарт для JavaBeans, а во fluent interface префикс set чаще опускают, чем оставляют.
  • +1
    Конструктор private NutritionFacts(Builder builder) для лёгкой экономии можно сделать не private, а с доступом по умолчанию (package), иначе будет автоматически сгенерирован ещё один невидимый «синтетический» конструктор. Ещё как вариант вместо явного вызова new NutritionFacts.Builder() можно сделать статический метод-фабрику.
    • 0
      про статическую фабрику вместо конструктора упомянуто. перечитайте внимательно.

      про синтетический конструктор согласен

  • +1
    спасибо, всё просто и понятно объяснили.
    мне паттерн понравился.
  • –1
    Также JavaBeans паттерн исключает возможность сделать класс неизменным(immutable), что требует дополнительных усилий со стороны программиста для обеспечения безопасности в многопоточной среде


    По-моему, использование synchronize на блоке кода:
    NutritionFacts cocaCola = new NutritionFacts();
    cocaCola.setServingSize(240);
    cocaCola.setServings(8);
    cocaCola.setCalories(100);
    cocaCola.setSodium(35);
    cocaCola.setCarbohydrate(27);

    призвано решать данную проблему
    • 0
      synchronize это дополнительные расходы
      • –1
        Было бы здорово, если бы Вы упомянули об этом в статье.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.