Перейти к основному содержимому

Конструкторы Java против статических фабричных методов

· 7 мин. чтения

1. Обзор

Конструкторы Java являются механизмом по умолчанию для получения полностью инициализированных экземпляров класса. В конце концов, они предоставляют всю инфраструктуру, необходимую для внедрения зависимостей, как вручную, так и автоматически.

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

В этом уроке мы выделим плюсы и минусы использования статических фабричных методов по сравнению с простыми старыми конструкторами Java .

2. Преимущества статических фабричных методов перед конструкторами

В объектно-ориентированном языке, таком как Java, что может быть не так с конструкторами? В общем, ничего. Тем не менее, знаменитый пункт 1 эффективного Java Джошуа Блока ясно гласит:

«Рассмотрите статические фабричные методы вместо конструкторов»

Хотя это не серебряная пуля, вот наиболее веские причины, поддерживающие такой подход:

  1. Конструкторы не имеют осмысленных имен , поэтому они всегда ограничиваются стандартными соглашениями об именах, налагаемыми языком. Статические фабричные методы могут иметь осмысленные имена , что явно указывает на то, что они делают.
  2. Статические фабричные методы могут возвращать тот же тип, который реализует метод(ы), подтип, а также примитивы , поэтому они предлагают более гибкий диапазон возвращаемых типов.
  3. Статические фабричные методы могут инкапсулировать всю логику, необходимую для предварительного создания полностью инициализированных экземпляров , поэтому их можно использовать для перемещения этой дополнительной логики из конструкторов. Это не позволяет конструкторам выполнять дальнейшие задачи, кроме инициализации полей.
  4. Статические фабричные методы могут быть методами с контролируемым экземпляром , причем шаблон Singleton является наиболее ярким примером этой функции.

3. Статические фабричные методы в JDK

В JDK есть множество примеров статических фабричных методов, демонстрирующих многие из описанных выше преимуществ. Давайте рассмотрим некоторые из них.

3.1. Строковый класс _

Из-за хорошо известного интернирования String очень маловероятно, что мы будем использовать конструктор класса String для создания нового объекта String . Тем не менее, это совершенно законно:

String value = new String("ForEach");

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

В качестве альтернативы, если мы хотим создать новый объект String с помощью статического фабричного метода , мы можем использовать некоторые из следующих реализаций метода valueOf() :

String value1 = String.valueOf(1);
String value2 = String.valueOf(1.0L);
String value3 = String.valueOf(true);
String value4 = String.valueOf('a');

Существует несколько перегруженных реализаций valueOf() . Каждый из них вернет новый объект String , в зависимости от типа аргумента, переданного методу (например , int , long , boolean , char и т. д.).

Название довольно ясно выражает то, что делает метод. Он также придерживается хорошо зарекомендовавшего себя в экосистеме Java стандарта именования статических фабричных методов.

3.2. Необязательный класс _

Еще одним ярким примером статических фабричных методов в JDK является класс Optional . Этот класс реализует несколько фабричных методов с довольно осмысленными именами , включая empty() , of() и ofNullable() :

Optional<String> value1 = Optional.empty();
Optional<String> value2 = Optional.of("ForEach");
Optional<String> value3 = Optional.ofNullable(null);

3.3. Класс коллекций _

Вполне возможно , что наиболее показательным примером статических фабричных методов в JDK является класс Collections . Это неинстансируемый класс, который реализует только статические методы.

Многие из них являются фабричными методами, которые также возвращают коллекции после применения к предоставленной коллекции некоторого типа алгоритма.

Вот несколько типичных примеров фабричных методов класса:

Collection syncedCollection = Collections.synchronizedCollection(originalCollection);
Set syncedSet = Collections.synchronizedSet(new HashSet());
List<Integer> unmodifiableList = Collections.unmodifiableList(originalList);
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(originalMap);

Количество статических фабричных методов в JDK очень велико, поэтому для краткости мы сократим список примеров.

Тем не менее приведенные выше примеры должны дать нам четкое представление о повсеместном распространении статических фабричных методов в Java.

4. Пользовательские статические фабричные методы

Конечно, мы можем реализовать свои собственные статические фабричные методы. Но когда это действительно стоит делать вместо того, чтобы создавать экземпляры классов с помощью простых конструкторов?

Давайте посмотрим на простой пример.

Давайте рассмотрим этот наивный класс User :

public class User {

private final String name;
private final String email;
private final String country;

public User(String name, String email, String country) {
this.name = name;
this.email = email;
this.country = country;
}

// standard getters / toString
}

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

Что, если мы хотим, чтобы все экземпляры User получили значение по умолчанию для поля страны ?

Если мы инициализируем поле значением по умолчанию, нам также придется реорганизовать конструктор, что сделает дизайн более жестким.

Вместо этого мы можем использовать статический фабричный метод:

public static User createWithDefaultCountry(String name, String email) {
return new User(name, email, "Argentina");
}

Вот как мы получим экземпляр User со значением по умолчанию, присвоенным полю страны :

User user = User.createWithDefaultCountry("John", "john@domain.com");

5. Перемещение логики из конструкторов

Наш класс User может быстро превратиться в несовершенный дизайн, если мы решим реализовать функции, которые потребуют добавления дополнительной логики в конструктор (к этому времени должны прозвучать тревожные звоночки).

Предположим, мы хотим предоставить классу возможность регистрировать время создания каждого объекта User .

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

Мы можем поддерживать чистоту нашего дизайна с помощью статического фабричного метода:

public class User {

private static final Logger LOGGER = Logger.getLogger(User.class.getName());
private final String name;
private final String email;
private final String country;

// standard constructors / getters

public static User createWithLoggedInstantiationTime(
String name, String email, String country) {
LOGGER.log(Level.INFO, "Creating User instance at : {0}", LocalTime.now());
return new User(name, email, country);
}
}

Вот как мы создадим наш улучшенный экземпляр User :

User user 
= User.createWithLoggedInstantiationTime("John", "john@domain.com", "Argentina");

6. Создание экземпляра, управляемого экземпляром

Как показано выше, мы можем инкапсулировать фрагменты логики в статические фабричные методы, прежде чем возвращать полностью инициализированные объекты User . И мы можем сделать это, не загрязняя конструктор ответственностью за выполнение нескольких несвязанных задач.

Например, предположим, что мы хотим сделать наш класс User одноэлементным. Мы можем добиться этого, реализуя статический фабричный метод, управляемый экземпляром:

public class User {

private static volatile User instance = null;

// other fields / standard constructors / getters

public static User getSingletonInstance(String name, String email, String country) {
if (instance == null) {
synchronized (User.class) {
if (instance == null) {
instance = new User(name, email, country);
}
}
}
return instance;
}
}

Реализация метода getSingletonInstance() является потокобезопасной, с небольшим снижением производительности из-за синхронизированного блока .

В этом случае мы использовали ленивую инициализацию, чтобы продемонстрировать реализацию статического фабричного метода, управляемого экземпляром.

Однако стоит отметить, что лучший способ реализовать Singleton — использовать тип перечисления Java , поскольку он безопасен как для сериализации, так и для потоков . Для получения полной информации о том, как реализовать синглтоны с использованием различных подходов, ознакомьтесь с этой статьей .

Как и ожидалось, получение объекта User с помощью этого метода очень похоже на предыдущие примеры:

User user = User.getSingletonInstance("John", "john@domain.com", "Argentina");

7. Заключение

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

Более того, этот шаблон рефакторинга настолько тесно связан с типичным рабочим процессом, что большинство IDE сделает это за нас.

Конечно, Apache NetBeans , IntelliJ IDEA и Eclipse будут выполнять рефакторинг немного по-разному, поэтому сначала обязательно проверьте документацию по IDE.

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

Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .