Constructors or Static Factory Methods?

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

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

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

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

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

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

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

  • They can cache.

  • They can subtype.

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

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

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

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

Однако правильным решением было бы использование полиморфизма и инкапсуляции для разложения проблемы на несколько семантически насыщенных классов.

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

See, Joshua?

They Can Cache

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-17 at 14:42

sixnines availability badge   GitHub stars