Привет, друг! Сегодня мы продолжим изучать с тобой паттерны проектирования. В этой лекции будем говорить о Фабрике. Обсудим с тобой, какую проблему решают с помощью данного шаблона, посмотрим на примере, как фабрика помогает открывать кофейню. А еще я дам тебе 5 простых шагов для создания фабрики.
Если коротко: Фабрика это шаблон проектирования, который забирает создание объектов себе. Ты передаешь фабрике условие, обычно значение
enum, а она возвращает готовый объект нужного класса-наследника. Вызовы
new перестают расползаться по программе: когда ассортимент меняется, править надо одно место, а не десять.
Чтобы быть со всеми на одной волне и легко улавливать суть, тебе должны быть знакомы такие темы:
Кратко
- Фабрика решает одну задачу: выбрать и создать нужного наследника в зависимости от условия, чтобы вызовы
new не расползались по коду.
- Условие удобнее передавать через
enum, чем строкой: список допустимых типов виден в одном месте, а опечатку поймает компилятор.
- Логика создания переезжает из метода кофейни в отдельный класс
SimpleCoffeeFactory с методом createCoffee.
- После этого ассортимент меняется в одном месте, а саму фабрику можно переиспользовать везде, где нужен кофе.
- Метод фабрики лучше не делать статическим: иначе не выйдет ни унаследоваться от нее, ни внедрить ее в другой класс через конструктор.
- Рецепт из пяти шагов: иерархия классов,
enum с типами, класс-фабрика, метод создания, switch по типу.
![Четыре цилиндрические емкости от оранжевой до желтой стоят в ряд на крыше промышленного здания]()
Что такое Фабрика?
Шаблон проектирования Фабрика позволяет управлять созданием объектов.
Процесс создания нового объекта не то чтобы прост, но и не слишком сложен. Все мы знаем, что для создания нового объекта необходимо использовать оператор
new. И может показаться, что здесь нечем управлять, однако это не так.
Сложности могут возникнуть, когда в нашем приложении есть некоторый класс, у которого есть множество наследников, и необходимо создавать экземпляр определенного класса в зависимости от некоторых условий.
Фабрика это шаблон проектирования, который помогает решить проблему создания различных объектов в зависимости от некоторых условий.
Абстрактно, не правда ли? Больше конкретики и ясности появится, когда мы рассмотрим пример ниже.
Создаем различные виды кофе
Предположим, мы хотим автоматизировать кофейню. Нам необходимо научиться готовить различные виды кофе. Для этого в нашем приложении мы создадим класс кофе и его производные: американо, капучино, эспрессо, латте, то есть те виды кофе, которые мы будем готовить.
Начнем с общего кофейного класса:
public class Coffee {
public void grindCoffee(){
// перемалываем кофе
}
public void makeCoffee(){
// делаем кофе
}
public void pourIntoCup(){
// наливаем в чашку
}
}
Далее создадим его наследников:
public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
Наши клиенты будут заказывать какой-либо вид кофе, и эту информацию нужно передавать программе. Это можно сделать разными способами, например использовать
String. Но лучше всего для этих целей подойдет
перечисление enum. Создадим
enum и определим в нем типы кофе, на которые мы принимаем заказы:
public enum CoffeeType {
ESPRESSO,
AMERICANO,
CAFFE_LATTE,
CAPPUCCINO
}
Отлично, теперь напишем код нашей кофейни:
public class CoffeeShop {
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new Americano();
break;
case ESPRESSO:
coffee = new Espresso();
break;
case CAPPUCCINO:
coffee = new Cappuccino();
break;
case CAFFE_LATTE:
coffee = new CaffeLatte();
break;
}
coffee.grindCoffee();
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Вот ваш кофе! Спасибо, приходите еще!");
return coffee;
}
}
Метод
orderCoffee можно разделить на две составляющие:
- Создание конкретного экземпляра кофе в блоке
switch-case. Именно здесь происходит то, что делает Фабрика: создание конкретного типа в зависимости от условий.
- Само приготовление: перемолка, приготовление и разлитие в чашку.
Что важно знать, если нужно будет вносить в метод изменения в будущем:
- Сам алгоритм приготовления (перемолка, приготовление и разлитие в чашку) останется неизменным (по крайней мере мы на это рассчитываем).
- А вот ассортимент кофе может измениться. Возможно, мы начнем готовить мока.. Мокка.. Моккачи… Господь с ним, новый вид кофе.
Мы уже сейчас можем предположить, что в будущем, с определенной долей вероятности, нам придется вносить изменения в метод, в блок
switch-case.
Также возможно, в нашей кофейне метод
orderCoffee будет не единственным местом, в котором мы будем создавать различные виды кофе. Следовательно, вносить изменения придется в нескольких местах.
Тебе уже наверняка понятно, к чему я клоню. Нам нужно рефакторить. Вынести блок, отвечающий за создание кофе, в отдельный класс по двум причинам:
- Мы сможем переиспользовать логику создания кофе в других местах.
- Если ассортимент изменится, нам не придется править код везде, где будет использоваться создание кофе. Достаточно будет изменить код только в одном месте.
Иными словами, пришло время запилить фабрику.
Пилим нашу первую фабрику
Для этого создадим новый класс, который будет отвечать только за создание нужных экземпляров классов кофе:
public class SimpleCoffeeFactory {
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new Americano();
break;
case ESPRESSO:
coffee = new Espresso();
break;
case CAPPUCCINO:
coffee = new Cappuccino();
break;
case CAFFE_LATTE:
coffee = new CaffeLatte();
break;
}
return coffee;
}
}
Поздравляю тебя! Мы только что реализовали шаблон проектирования Фабрика в самом его простейшем виде.
Небольшая ремарка про версии Java. Связка
Coffee coffee = null; плюс
switch с
break в каждой ветке это классика, но с Java 14 у
switch появилась вторая форма, switch-выражение. Оно не выполняет действия, а возвращает значение, поэтому ту же фабрику можно записать так:
public class SimpleCoffeeFactory {
public Coffee createCoffee(CoffeeType type) {
return switch (type) {
case AMERICANO -> new Americano();
case ESPRESSO -> new Espresso();
case CAPPUCCINO -> new Cappuccino();
case CAFFE_LATTE -> new CaffeLatte();
};
}
}
Пропали переменная
coffee, четыре
break и возможность случайно провалиться из одной ветки в соседнюю. Но главное не это. Ветки switch-выражения обязаны покрывать все константы
enum: добавишь в
CoffeeType новый вид кофе и забудешь про фабрику, и проект просто не соберется. Старый вариант в той же ситуации молча вернет
null, а упадет все позже и в другом месте, на строчке
coffee.grindCoffee(). Для статьи, вся завязка которой держится на фразе «ассортимент кофе может измениться», довод весомый. Разбор обеих форм
switch есть в
документации Oracle.
Хотя все могло быть еще проще, если сделать метод
createCoffee статичным. Но тогда мы потеряли бы две возможности:
- Наследоваться от
SimpleCoffeeFactory и переопределять метод createCoffee.
- Внедрять нужную реализацию фабрики в наши классы.
Кстати о внедрении. Нам нужно вернуться в кофейню и внедрить нашу фабрику по созданию кофе.
Внедрение фабрики в кофейню
Перепишем класс нашей кофейни с использованием фабрики:
public class CoffeeShop {
private final SimpleCoffeeFactory coffeeFactory;
public CoffeeShop(SimpleCoffeeFactory coffeeFactory) {
this.coffeeFactory = coffeeFactory;
}
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = coffeeFactory.createCoffee(type);
coffee.grindCoffee();
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Вот ваш кофе! Спасибо, приходите еще!");
return coffee;
}
}
Отлично. Теперь схематично и лаконично попробуем описать структуру шаблона проектирования Фабрика.
5 шагов к открытию собственной фабрики
Шаг 1. У тебя в программе класс с несколькими потомками, как на картинке ниже:
Шаг 2. Ты создаешь
enum, в котором определяешь enum-переменную для каждого класса-наследника:
enum CatType {
LION,
TIGER,
BARSIK
}
Шаг 3. Ты строишь свою фабрику. Называешь ее по имени класса, в нашем примере это
CatFactory, код ниже:
class CatFactory {}
Шаг 4. Ты создаешь в своей фабрике метод создания, тоже названный по классу, в нашем примере это
createCat. Он принимает переменную-
enum типа
CatType. Код ниже:
class CatFactory {
public Cat createCat(CatType type) {
}
}
Шаг 5. Ты пишешь в теле метода блок
switch-case, в котором перебираешь все enum значения и создаешь экземпляр класса, соответствующий
enum значению:
class CatFactory {
public Cat createCat(CatType type) {
Cat cat = null;
switch (type) {
case LION:
cat = new Lion();
break;
case TIGER:
cat = new Tiger();
break;
case BARSIK:
cat = new Barsik();
break;
}
return cat;
}
}
Like a boss.
Фабрика, фабричный метод и абстрактная фабрика
То, что мы собрали выше, обычно называют простой фабрикой (Simple Factory). В классическом каталоге GoF такого паттерна нет: там есть фабричный метод и абстрактная фабрика. Простая фабрика это упрощенный прием, с которого удобно начинать, и из него вырастают оба взрослых паттерна.
| Паттерн |
Кто решает, какой объект создать |
Когда пригодится |
| Простая фабрика |
Один класс с методом, который смотрит на переданное условие |
Создание объектов одной иерархии разбросано по коду, и его надо собрать в одном месте |
| Фабричный метод |
Наследник фабрики, который переопределяет метод создания |
Алгоритм общий, но каждая его разновидность должна создавать свои объекты |
| Абстрактная фабрика |
Отдельная фабрика на каждое семейство объектов |
Нужно создавать сразу несколько связанных объектов и не смешивать семейства |
Следующий шаг после этой статьи это
фабричный метод: в нем выбор конкретного класса переезжает из
switch в наследников самой фабрики.
Как тренироваться
Читать хорошо, а писать код еще лучше. Если в твоем имени четное количество букв, попробуй создать свою виртуальную пиццерию.
Если в твоем имени нечетное количество букв, попробуй создать виртуальный суши-бар.
Если ты безымянный, тебе повезло. Сегодня можешь отдыхать.
Вопросы и ответы
Что такое паттерн Фабрика в Java?
Фабрика это шаблон проектирования, который отвечает за создание объектов. Вместо того чтобы в разных местах программы писать
new Americano() или
new Espresso(), ты вызываешь один метод фабрики и передаешь ему условие, а она решает, объект какого класса вернуть. Код, который пользуется объектами, перестает зависеть от того, как именно они создаются.
Зачем нужна фабрика, если объект можно создать через new?
Ради одного места для изменений. Пока видов кофе четыре и создаются они в одном методе,
new вполне достаточно. Проблемы начинаются, когда наследников десяток, а создают их в пяти разных местах: добавили новый вид, и надо не забыть поправить все пять. Фабрика собирает эту логику в один класс, который вдобавок можно переиспользовать и подменять.
Почему тип объекта передают через enum, а не строкой?
Строка ничем не ограничена: опечатка вроде
"esspresso" спокойно скомпилируется и проявится только во время работы программы. У
enum набор значений фиксирован, опечатку поймает компилятор, среда разработки подскажет варианты, а весь список типов, на которые кофейня принимает заказы, лежит в одном файле.
Почему метод фабрики не стоит делать статическим?
Статический метод нельзя переопределить в наследнике, и объект фабрики со статическим методом некуда передать. Из-за этого пропадают две возможности: сделать наследника
SimpleCoffeeFactory со своим набором кофе и внедрить нужную фабрику в кофейню через конструктор, как сделано выше в статье.
Чем простая фабрика отличается от фабричного метода?
В простой фабрике решение принимает один класс: он смотрит на переданное значение
enum и в
switch выбирает, что создать. В фабричном методе выбора внутри метода нет, вместо него работает наследование: базовый класс объявляет метод создания, а каждый наследник возвращает свой объект. Простая фабрика расширяется правкой
switch, фабричный метод дописыванием нового наследника.
Есть ли фабрики в самой Java?
Да, и много.
Calendar.getInstance() возвращает календарь, подходящий для текущей локали,
NumberFormat.getInstance() отдает готовый форматтер чисел,
DocumentBuilderFactory.newInstance() создает парсер XML. Документация Oracle прямо называет такие методы фабричными и предупреждает, что за
NumberFormat может скрываться
DecimalFormat или
CompactNumberFormat, смотря какой метод вызван:
javadoc NumberFormat.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ