Pull to refresh

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

Reading time 7 min
Views 39K
Статья представляет вольный перевод главы из книги 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, подобный подход улучшит читабельность кода.

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

Tags:
Hubs:
+37
Comments 52
Comments Comments 52

Articles