Статьи

Конструкторы или статические фабричные методы?

Я считаю, что Джошуа Блох впервые сказал это в своей очень хорошей книге «Эффективная Java» : статические фабричные методы являются предпочтительным способом создания объектов по сравнению с конструкторами. Я не согласен. Не только потому, что я считаю статические методы чистым злом, но в основном потому, что в данном конкретном случае они притворяются добрыми и заставляют нас думать, что мы должны их любить.

Извлечение (2009) Майком Джаджем

Давайте проанализируем рассуждения и посмотрим, почему это неправильно, с объектно-ориентированной точки зрения.

Это класс с одним основным и двумя дополнительными конструкторами:

01
02
03
04
05
06
07
08
09
10
11
12
class Color {
  private final int hex;
  Color(String rgb) {
    this(Integer.parseInt(rgb, 16));
  }
  Color(int red, int green, int blue) {
    this(red << 16 + green << 8 + blue);
  }
  Color(int h) {
    this.hex = h;
  }
}

Это аналогичный класс с тремя статическими фабричными методами:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Color {
  private final int hex;
  static Color makeFromRGB(String rgb) {
    return new Color(Integer.parseInt(rgb, 16));
  }
  static Color makeFromPalette(int red, int green, int blue) {
    return new Color(red << 16 + green << 8 + blue);
  }
  static Color makeFromHex(int h) {
    return new Color(h);
  }
  private Color(int h) {
    return new Color(h);
  }
}

Какой из них вам больше нравится?

По словам Джошуа Блоха, использование статических фабричных методов вместо конструкторов имеет три основных преимущества (на самом деле их четыре, но четвертое больше не применимо к Java):

  • У них есть имена.
  • Они могут кешировать.
  • Они могут подтип.

Я считаю, что все три имеют смысл … если дизайн не так. Это хорошие оправдания для обходных путей. Давайте возьмем их один за другим.

У них есть имена

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

1
Color tomato = new Color(255, 99, 71);

Вот как вы делаете это с помощью статического метода фабрики:

1
Color tomato = Color.makeFromPalette(255, 99, 71);

Кажется, что makeFromPalette() семантически более makeFromPalette() чем просто new Color() , верно? Ну да. Кто знает, что означают эти три числа, если мы просто передадим их конструктору. Но слово «палитра» помогает нам сразу все понять.

Правда.

Тем не менее, правильным решением было бы использовать полиморфизм и инкапсуляцию, чтобы разложить проблему на несколько семантически богатых классов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
interface Color {
}
class HexColor implements Color {
  private final int hex;
  HexColor(int h) {
    this.hex = h;
  }
}
class RGBColor implements Color {
  private final Color origin;
  RGBColor(int red, int green, int blue) {
    this.origin = new HexColor(
      red << 16 + green << 8 + blue
    );
  }
}

Теперь мы используем правильный конструктор правильного класса:

1
Color tomato = new RGBColor(255, 99, 71);

Видишь, Джошуа?

Они могут кешировать

Допустим, мне нужен красный томатный цвет в нескольких местах приложения:

1
2
3
Color tomato = new Color(255, 99, 71);
// ... sometime later
Color red = new Color(255, 99, 71);

Будут созданы два объекта, что, очевидно, неэффективно, поскольку они идентичны. Было бы лучше сохранить первый экземпляр где-нибудь в памяти и вернуть его при поступлении второго вызова. Статические фабричные методы позволяют решить эту проблему:

1
2
3
Color tomato = Color.makeFromPalette(255, 99, 71);
// ... sometime later
Color red = Color.makeFromPalette(255, 99, 71);

Затем где-то внутри Color мы сохраняем приватную статическую Map со всеми объектами, уже созданными:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class Color {
  private static final Map<Integer, Color> CACHE =
    new HashMap<>();
  private final int hex;
  static Color makeFromPalette(int red, int green, int blue) {
    final int hex = red << 16 + green << 8 + blue;
    return Color.CACHE.computeIfAbsent(
      hex, h -> new Color(h)
    );
  }
  private Color(int h) {
    return new Color(h);
  }
}

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

Правда.

Однако существует объектно-ориентированный способ решения этой проблемы. Мы просто представляем новый класс Palette , который становится магазином цветов:

01
02
03
04
05
06
07
08
09
10
class Palette {
  private final Map<Integer, Color> colors =
    new HashMap<>();
  Color take(int red, int green, int blue) {
    final int hex = red << 16 + green << 8 + blue;
    return this.computerIfAbsent(
      hex, h -> new Color(h)
    );
  }
}

Теперь мы делаем экземпляр Palette один раз и просим его возвращать нам цвет каждый раз, когда он нам нужен:

1
2
3
Color tomato = palette.take(255, 99, 71);
// Later we will get the same instance:
Color red = palette.take(255, 99, 71);

Видите, Джошуа, нет статических методов, нет статических атрибутов.

Они могут подтип

Допустим, у нашего класса Color есть метод lighter() , который должен сместить цвет на следующий доступный более светлый:

1
2
3
4
5
6
7
8
9
class Color {
  protected final int hex;
  Color(int h) {
    this.hex = h;
  }
  public Color lighter() {
    return new Color(hex + 0x111);
  }
}

Однако иногда более желательно выбрать следующий более светлый цвет через набор доступных цветов Pantone :

01
02
03
04
05
06
07
08
09
10
11
12
13
class PantoneColor extends Color {
  private final PantoneName pantone;
  PantoneColor(String name) {
    this(new PantoneName(name));
  }
  PantoneColor(PantoneName name) {
    this.pantone = name;
  }
  @Override
  public Color lighter() {
    return new PantoneColor(this.pantone.up());
  }
}

Затем мы создаем статический метод фабрики, который решит, какая реализация Color наиболее подходит для нас:

1
2
3
4
5
6
7
8
9
class Color {
  private final String code;
  static Color make(int h) {
    if (h == 0xBF1932) {
      return new PantoneColor("19-1664 TPX");
    }
    return new RGBColor(h);
  }
}

Если запрашивается истинно красный цвет, мы возвращаем экземпляр PantoneColor . Во всех остальных случаях это просто стандартный RGBColor . Решение принимается статическим фабричным методом. Вот как мы будем называть это:

1
Color color = Color.make(0xBF1932);

Было бы невозможно сделать то же самое «разветвление» с конструктором, так как он может возвращать только тот класс, в котором он объявлен. Статический метод обладает всей необходимой свободой для возврата любого подтипа Color .

Правда.

Однако в объектно-ориентированном мире мы можем и должны делать все по-другому. Во-первых, мы бы сделали Color интерфейсом:

1
2
3
interface Color {
  Color lighter();
}

Затем мы перенесем этот процесс принятия решений на собственный класс Colors , как мы это делали в предыдущем примере:

1
2
3
4
5
6
7
8
class Colors {
  Color make(int h) {
    if (h == 0xBF1932) {
      return new PantoneColor("19-1664-TPX");
    }
    return new RGBColor(h);
  }
}

И мы бы использовали экземпляр класса Colors вместо статического метода factory внутри Color :

1
colors.make(0xBF1932);

Тем не менее, это по-прежнему не объектно-ориентированный способ мышления, потому что мы отдаляем процесс принятия решений от объекта, которому он принадлежит. Либо с помощью статического метода фабрики make() либо с помощью нового класса Colors — это не имеет значения, как — мы разрываем наши объекты на две части. Первая часть — это сам объект, а вторая — алгоритм принятия решений, который находится где-то еще.

Гораздо более объектно-ориентированный дизайн заключался бы в том, чтобы поместить логику в объект класса PantoneColor который бы украшал исходный RGBColor :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class PantoneColor {
  private final Color origin;
  PantoneColor(Color color) {
    this.origin = color;
  }
  @Override
  public Color lighter() {
    final Color next;
    if (this.origin.hex() == 0xBF1932) {
      next = new RGBColor(0xD12631);
    } else {
      next = this.origin.lighter();
    }
    return new PantoneColor(next);
  }
)

Затем мы создаем экземпляр RGBColor и украшаем его с помощью PantoneColor :

1
2
3
Color red = new PantoneColor(
  new RGBColor(0xBF1932)
);

Мы просим red вернуть более светлый цвет, и он возвращает тот из палитры Pantone, а не тот, который просто светлее в координатах RGB:

1
Color lighter = red.lighter(); // 0xD12631

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

Подводя итог, я настоятельно рекомендую вам никогда не использовать статические методы, особенно когда они собираются заменить конструкторы объектов. Рождение объекта через его конструктор — самый «священный» момент в любом объектно-ориентированном программном обеспечении, не упустите его красоту.

Вы также можете найти эти посты интересными: каждый частный статический метод является кандидатом в новый класс ; Чем лучше вы архитектор, тем проще ваши диаграммы ; Может быть только один первичный конструктор ; Почему дизайн InputStream неправильный ; Почему многие заявления о возвращении являются плохой идеей в ООП ;

Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Конструкторы или статические фабричные методы?

Мнения, высказанные участниками Java Code Geeks, являются их собственными.