Статьи

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

Ранее я немного рассказал о паттерне Builder , полезном паттерне для создания экземпляров классов с несколькими (возможно, необязательными) атрибутами, что облегчает чтение, запись и обслуживание клиентского кода, а также другие преимущества. Сегодня я собираюсь продолжить изучение методов создания объектов, но на этот раз для более общего случая.

Возьмите следующий пример, который ни в коем случае не является полезным классом, кроме как для моей цели. У нас есть класс RandomIntGenerator, который, как следует из названия, генерирует случайные целые числа. Что-то типа:

1
2
3
4
5
6
public class RandomIntGenerator {
    private final int min;
    private final int max;
 
    public int next() {...}
}

Наш генератор берет минимум и максимум, а затем генерирует случайные числа между этими двумя значениями. Обратите внимание, что эти два атрибута объявлены как final, поэтому мы должны инициализировать их либо в их объявлении, либо в конструкторе класса. Давайте пойдем с конструктором:

1
2
3
4
public RandomIntGenerator(int min, int max) {
    this.min = min;
    this.max = max;
}

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

1
2
3
4
public RandomIntGenerator(int min) {
    this.min = min;
    this.max = Integer.MAX_VALUE;
}

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

1
2
3
4
public RandomIntGenerator(int max) {
    this.min = Integer.MIN_VALUE;
    this.max = max;
}

Если вы попробуете это, вы получите ошибку компиляции: Дублируйте метод RandomIntGenerator (int) в типе RandomIntGenerator . Что случилось? Проблема в том, что конструкторы по определению не имеют имен. Таким образом, у класса может быть только один конструктор с заданной сигнатурой, так же как у вас не может быть двух методов с одинаковой сигнатурой (один и тот же тип возврата, имя и тип параметров). Вот почему, когда мы попытались добавить конструктор RandomIntGenerator (int max), мы получили эту ошибку компиляции, потому что у нас уже была ошибка RandomIntGenerator (int min) .

Есть ли что-то, что мы можем сделать в подобных случаях? Не с конструкторами, но, к счастью, мы можем использовать что-то еще: статические фабричные методы , которые являются просто публичными статическими методами, которые возвращают экземпляр класса. Вы, вероятно, использовали эту технику, даже не осознавая этого. Вы когда-нибудь использовали Boolean.valueOf ? Это выглядит примерно так:

1
2
3
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

Применяя статические фабрики к нашему примеру RandomIntGenerator , мы можем получить:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RandomIntGenerator {
    private final int min;
    private final int max;
 
    private RandomIntGenerator(int min, int max) {
        this.min = min;
        this.max = max;
    }
 
    public static RandomIntGenerator between(int max, int min) {
        return new RandomIntGenerator(min, max);
    }
 
    public static RandomIntGenerator biggerThan(int min) {
        return new RandomIntGenerator(min, Integer.MAX_VALUE);
    }
 
    public static RandomIntGenerator smallerThan(int max) {
        return new RandomIntGenerator(Integer.MIN_VALUE, max);
    }
 
    public int next() {...}
}

Обратите внимание, как конструктор стал закрытым, чтобы гарантировать, что класс создается только через его общедоступные статические методы фабрики. Также обратите внимание, как четко выражаются ваши намерения, когда у вас есть клиент с RandomIntGenerator.between(10,20) вместо new RandomIntGenerator(10,20) . Стоит отметить, что эта методика не совпадает с фабричным методом Design Pattern от Gang of Four . Любой класс может предоставлять статические фабричные методы вместо конструкторов или в дополнение к ним. Так каковы преимущества и недостатки этого метода? Мы уже упоминали первое преимущество статических фабричных методов: в отличие от конструкторов они имеют имена. Это имеет два прямых последствия,

  1. Мы можем предоставить значимое имя для наших конструкторов.
  2. Мы можем предоставить несколько конструкторов с одинаковым числом и типом параметров, что, как мы видели ранее, мы не можем сделать с конструкторами классов.

Еще одним преимуществом статических фабрик является то, что, в отличие от конструкторов, они не обязаны возвращать новый объект каждый раз, когда они вызываются. Это чрезвычайно полезно при работе с неизменяемыми классами, чтобы обеспечить постоянные объекты для часто используемых значений и избежать создания ненужных дублирующих объектов. Код Boolean.valueOf который я показал ранее, прекрасно иллюстрирует этот момент. Обратите внимание, что этот статический метод возвращает либо TRUE либо FALSE , оба неизменяемых логических объекта.

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

Помните RandomIntGenerator в начале этого поста? Давайте сделаем это немного сложнее. Представьте, что теперь мы хотим предоставить случайные генераторы не только для целых чисел, но и для других типов данных, таких как String, Double или Long. Все они будут иметь метод next() который возвращает случайный объект определенного типа, поэтому мы могли бы начать с интерфейса, подобного следующему:

1
2
3
public interface RandomGenerator<T> {
    T next();
}

Наша первая реализация RandomIntGenerator теперь становится:

01
02
03
04
05
06
07
08
09
10
11
class RandomIntGenerator implements RandomGenerator<Integer> {
    private final int min;
    private final int max;
 
    RandomIntGenerator(int min, int max) {
        this.min = min;
        this.max = max;
    }
 
    public Integer next() {...}
}

У нас также может быть генератор строк:

1
2
3
4
5
6
7
8
9
class RandomStringGenerator implements RandomGenerator<String> {
    private final String prefix;
 
    RandomStringGenerator(String prefix) {
        this.prefix = prefix;
    }
 
    public String next() {...}
}

Обратите внимание, как все классы объявлены как закрытые для пакета (область по умолчанию), так же как и их конструкторы. Это означает, что ни один клиент вне своего пакета не может создавать экземпляры этих генераторов. Так что же нам делать? Подсказка: начинается со «статического» и заканчивается «методами».
Рассмотрим следующий класс:

01
02
03
04
05
06
07
08
09
10
11
12
public final class RandomGenerators {
    // Suppresses default constructor, ensuring non-instantiability.
    private RandomGenerators() {}
 
    public static final RandomGenerator<Integer> getIntGenerator() {
        return new RandomIntGenerator(Integer.MIN_VALUE, Integer.MAX_VALUE);
    }
 
    public static final RandomGenerator<String> getStringGenerator() {
        return new RandomStringGenerator('');
    }
}

RandomGenerators — это просто нереализуемый служебный класс, в котором нет ничего, кроме статических фабричных методов. Находясь в одном пакете с различными генераторами, этот класс может эффективно обращаться к этим классам и создавать их экземпляры. Но здесь начинается интересная часть. Обратите внимание, что методы возвращают RandomGenerator интерфейс RandomGenerator , и это все, что действительно нужно клиентам. Если они получают RandomGenerator<Integer> они знают, что могут вызвать next() и получить случайное целое число.
Представьте, что в следующем месяце мы создадим супер эффективный генератор целых чисел. При условии, что этот новый класс реализует RandomGenerator<Integer> мы можем изменить тип возврата статического метода фабрики, и все клиенты теперь волшебным образом используют новую реализацию, даже не замечая этого изменения.

Такие классы, как RandomGenerators , довольно распространены как в JDK, так и в сторонних библиотеках. Вы можете увидеть примеры в Collections (в java.util), Lists , Sets или Maps в Гуаве . Соглашение о присвоении имен обычно одинаковое: если у вас есть интерфейс с именем Type вы помещаете ваши статические фабричные методы в непостижимый класс с именем Types .

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

1
Map<String, List<String>> map = new HashMap<String, List<String>>();

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

1
2
3
public static <K, V> HashMap<K, V> newHashMap() {
  return new HashMap<K, V>();
}

Итак, теперь наш клиентский код становится:

1
Map<String, List<String>> map = Maps.newHashMap();

Довольно мило, не правда ли? Эта возможность известна как вывод типа . Стоит отметить, что в Java 7 введен вывод типов благодаря использованию оператора diamond . Так что если вы используете Java 7, вы можете написать предыдущий пример как:

1
Map<String, List<String>> map = new HashMap<>();

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

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

Ссылка: Статические методы фабрики против традиционных конструкторов от нашего партнера JCG Хосе Луиса в разработке так, как это должно быть в блоге.