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