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

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

1. обзор

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

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

В этом руководстве мы будем выделятьpros and cons of using static factory methods vs plain old Java constructors.

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

В объектно-ориентированном языке, таком как Java, что может быть не так с конструкторами? В целом ничего. Тем не менее, знаменитыйJoshua Block’s Effective Java Item 1 четко утверждает:

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

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

  1. Constructors don’t have meaningful names, поэтому они всегда ограничиваются стандартным соглашением об именах, наложенным языком. Static factory methods can have meaningful names, поэтому явно передают, что они делают

  2. Static factory methods can return the same type that implements the method(s), a subtype, and also primitives, поэтому они предлагают более гибкий диапазон возвращаемых типов

  3. Static factory methods can encapsulate all the logic required for pre-constructing fully initialized instances, поэтому их можно использовать для удаления этой дополнительной логики из конструкторов. Это предотвращает конструкторы изperforming further tasks, others than just initializing fields

  4. Static factory methods can be controlled-instanced methods, причемSingleton pattern является наиболее ярким примером этой функции

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

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

3.1. КлассString

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

String value = new String("example");

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

В качестве альтернативы, если мы хотимcreate a new String object using a static factory method, мы можем использовать некоторые из следующих реализаций метода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. КлассOptional

Еще один изящный пример статических фабричных методов в JDK - это классOptional. Этот классimplements a few factory methods with pretty meaningful names, включаяempty(),of() иofNullable():

Optional value1 = Optional.empty();
Optional value2 = Optional.of("example");
Optional value3 = Optional.ofNullable(null);

3.3. КлассCollections

Вполне возможно,the most representative example of static factory methods in the JDK is the Collections class. Это класс, не поддерживающий создание экземпляров, который реализует только статические методы.

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

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

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

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

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

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

Конечно,we can implement our own static factory methods.. Но когда это действительно стоит делать, вместо того, чтобы создавать экземпляры классов с помощью простых конструкторов?

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

Давайте рассмотрим этот наивный класс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 получали значение по умолчанию для поляcountry?

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

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

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

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

User user = User.createWithDefaultCountry("John", "[email protected]");

5. Вывод логики из конструкторов

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

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

If we just put this logic into the constructor, we’d be breaking the Single Responsibility Principle. В итоге мы получим монолитный конструктор, который делает гораздо больше, чем инициализирует поля.

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

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) {
        setLoggerProperties();
        LOGGER.log(Level.INFO, "Creating User instance at : {0}", LocalTime.now());
        return new User(name, email, country);
    }

    private static void setLoggerProperties() {
        ConsoleHandler handler = new ConsoleHandler();
        handler.setLevel(Level.INFO);
        handler.setFormatter(new SimpleFormatter());
        LOGGER.addHandler(handler);
    }
}

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

User user
  = User.createWithLoggedInstantiationTime("John", "[email protected]", "Argentina");

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

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

Например,suppose we want to make our User class a Singleton. We can achieve this by implementing an instance-controlled static factory method:

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() -thread-safe, with a small performance penalty, due to the synchronized block.

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

Однако стоит отметить, чтоthe best way to implement a Singleton is with a Java enum type, as it’s both serialization-safe and thread-safe. Для получения полной информации о том, как реализовать синглтоны с использованием различных подходов, проверьтеthis article.

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

User user = User.getSingletonInstance("John", "[email protected]", "Argentina");

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

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

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

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

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

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