Фабричный метод это порождающий паттерн, при котором базовый класс описывает, что нужно делать с объектом, но сам его не создает. Создание вынесено в отдельный метод, который объявлен абстрактным и переопределяется в наследниках: каждый наследник решает, объект какого конкретного класса вернуть. В нашем примере это будет
CoffeeShop с абстрактным методом
createCoffee(): сама кофейня знает, как принять и выдать заказ, а какой именно кофе варить, решают итальянский и американский филиалы.
Кратко
- Фабричный метод нужен, когда одна и та же логика должна работать с несколькими семействами продуктов: у нас это кофейня, которая варит кофе в итальянском и в американском стиле.
- Класс-создатель делают абстрактным, а метод создания продукта абстрактным методом. Всю остальную работу с продуктом создатель берет на себя.
- Наследники создателя переопределяют этот метод и возвращают свои реализации продукта. Суперкласс о них ничего не знает.
- Получаются две параллельные иерархии: иерархия продуктов и иерархия создателей.
- Отличие от простой фабрики: там фабрика это отдельный объект, который передают снаружи, а тут создание живет внутри самого создателя и меняется наследованием.
- Все продукты обязаны наследоваться от общего класса или интерфейса, иначе клиентский код не сможет работать с ними единообразно.
Привет! Сегодня мы продолжим изучать
паттерны проектирования и поговорим о фабричном методе (FactoryMethod).
![Резервуары и трубы старого завода на фоне сумеречного неба с луной]()
Ты узнаешь, что это такое и для решения каких задач подходит данный шаблон. Мы рассмотрим этот паттерн проектирования на практике и изучим его структуру.
Чтобы все изложенное было тебе понятно, необходимо разбираться в следующих темах:
- Наследование в Java.
- Абстрактные методы и классы в Java.
Какую проблему решает фабричный метод
Во всех фабричных паттернах проектирования есть две группы участников: создатели (сами фабрики) и продукты (объекты, создаваемые фабриками).
Представь ситуацию: у нас есть фабрика, выпускающая автомобили под маркой AutoRush. Она умеет создавать модели автомобилей с различными видами кузовов:
Дела у нас шли настолько хорошо, что в один прекрасный день мы поглотили концерн OneAuto.
Как здравомыслящие управленцы, мы не хотим терять клиентов OneAuto, и перед нами стоит задача реструктурировать производство таким образом, чтобы мы могли выпускать:
- седаны AutoRush
- универсалы AutoRush
- купе AutoRush
- седаны OneAuto
- универсалы OneAuto
- купе OneAuto
Как видишь, вместо одной группы производных продуктов появилось две, которые различаются некоторыми деталями.
Шаблон проектирования
фабричный метод решает проблему создания различных групп продуктов, каждая из которых обладает некоторой спецификой.
Принцип данного шаблона мы рассмотрим на практике, постепенно переходя от простого к сложному, на примере нашей кофейни, которую мы создали в одной из
предыдущих лекций.
Немного о шаблоне фабрика
Напомню: мы построили с тобой небольшую виртуальную кофейню. В ней мы с помощью простой фабрики научились создавать различные виды кофе. Сегодня будем дорабатывать данный пример.
Давай вспомним, как выглядела наша кофейня с простой фабрикой.
У нас был класс кофе:
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 {}
Для удобства принятия заказов мы завели перечисления:
public enum CoffeeType {
ESPRESSO,
AMERICANO,
CAFFE_LATTE,
CAPPUCCINO
}
Сама фабрика по производству кофе выглядела следующим образом:
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;
}
}
Ну и, наконец, сама кофейня:
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;
}
}
Модернизация простой фабрики
Наша кофейня работает хорошо. Настолько, что мы подумываем о расширении.
Мы хотим открыть несколько новых точек. Как предприимчивые ребята, мы не будем штамповать однообразные кофейни. Хочется, чтобы у каждой была изюминка.
Поэтому для начала откроем две точки: в итальянском и американском стилях.
Изменения затронут не только интерьер, но и напитки:
- в итальянской кофейне мы будем использовать исключительно итальянские кофейные бренды, с особым помолом и прожаркой.
- в американской порции будут чуточку больше, и к каждому заказу будем подавать плавленный зефир, маршмеллоу.
Единственное, что останется неизменным, это наша бизнес-модель, которая хорошо зарекомендовала себя.
Если говорить на языке кода, то вот что получается. У нас было 4 класса продуктов:
public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
А станет 8:
public class ItalianStyleAmericano extends Coffee {}
public class ItalianStyleCappuccino extends Coffee {}
public class ItalianStyleCaffeLatte extends Coffee {}
public class ItalianStyleEspresso extends Coffee {}
public class AmericanStyleAmericano extends Coffee {}
public class AmericanStyleCappuccino extends Coffee {}
public class AmericanStyleCaffeLatte extends Coffee {}
public class AmericanStyleEspresso extends Coffee {}
Раз мы желаем сохранить действующую бизнес-модель неизменной, нам хочется, чтобы метод
orderCoffee(CoffeeType type) претерпел минимальное количество изменений.
Взглянем на него:
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = coffeeFactory.createCoffee(type);
coffee.grindCoffee();
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Вот ваш кофе! Спасибо, приходите еще!");
return coffee;
}
Наивное решение: две фабрики вместо одной
Какие варианты у нас есть? Мы ведь уже умеем писать фабрику? Самое простое, что сходу приходит в голову, это написать две аналогичные фабрики, а затем передавать нужную реализацию в нашу кофейню в конструкторе. Тогда класс кофейни не изменится.
Для начала нам нужно создать новый класс-фабрику, унаследоваться от нашей простой фабрики и переопределить метод
createCoffee (CoffeeType type). Напишем фабрики для создания кофе в итальянском и американском стилях:
public class SimpleItalianCoffeeFactory extends SimpleCoffeeFactory {
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new ItalianStyleAmericano();
break;
case ESPRESSO:
coffee = new ItalianStyleEspresso();
break;
case CAPPUCCINO:
coffee = new ItalianStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new ItalianStyleCaffeLatte();
break;
}
return coffee;
}
}
public class SimpleAmericanCoffeeFactory extends SimpleCoffeeFactory{
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new AmericanStyleAmericano();
break;
case ESPRESSO:
coffee = new AmericanStyleEspresso();
break;
case CAPPUCCINO:
coffee = new AmericanStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new AmericanStyleCaffeLatte();
break;
}
return coffee;
}
}
Теперь мы можем передавать нужную реализацию фабрики в CoffeeShop. Давай посмотрим, как бы выглядел код для заказа кофе из разных кофеен. Например, капучино в итальянском и американском стилях:
public class Main {
public static void main(String[] args) {
/*
Закажем капучино в итальянском стиле:
1. Создадим фабрику для приготовления итальянского кофе
2. Создадим новую кофейню, передав ей в конструкторе фабрику итальянского кофе
3. Закажем наш кофе
*/
SimpleItalianCoffeeFactory italianCoffeeFactory = new SimpleItalianCoffeeFactory();
CoffeeShop italianCoffeeShop = new CoffeeShop(italianCoffeeFactory);
italianCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
/*
Закажем капучино в американском стиле
1. Создадим фабрику для приготовления американского кофе
2. Создадим новую кофейню, передав ей в конструкторе фабрику американского кофе
3. Закажем наш кофе
*/
SimpleAmericanCoffeeFactory americanCoffeeFactory = new SimpleAmericanCoffeeFactory();
CoffeeShop americanCoffeeShop = new CoffeeShop(americanCoffeeFactory);
americanCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
}
}
Чем плох такой подход
Мы создали две различные кофейни, передав в каждую нужную фабрику. С одной стороны, мы достигли поставленной задачи, но с другой стороны... Что-то скребет неуемную душу предпринимателя… Давай разбираться, что не так.
Во-первых, обилие фабрик. Это что, каждый раз теперь под новую точку свою фабрику создавать и вдобавок следить за тем, чтобы при создании кофейни в конструктор передавалась нужная фабрика?
Во-вторых, это все еще простая фабрика. Просто немного модернизированная. Мы тут все-таки новый паттерн изучаем.
В-третьих, а что, нельзя что ли по-другому? Вот было бы классно, если бы мы могли локализовать все вопросы по приготовлению кофе внутри класса
CoffeeShop, связав процессы по созданию кофе и обслуживанию заказа, но при этом сохранив достаточную гибкость, чтобы делать кофе в различных стилях.
Ответ: да, можно. Это называется шаблон проектирования фабричный метод.
От простой фабрики к фабричному методу
Чтобы решить поставленную задачу максимально эффективно, мы:
- Вернем метод
createCoffee(CoffeeType type) в класс CoffeeShop.
- Данный метод сделаем абстрактным.
- Сам класс
CoffeeShop станет абстрактным.
- У класса
CoffeeShop появятся наследники.
Да, друг. Итальянская кофейня это не что иное, как наследник класса
CoffeeShop, реализующий метод
createCoffee(CoffeeType type) в соответствии с лучшими традициями итальянских бариста.
Итак, по порядку.
Шаг 1. Класс Coffee становится абстрактным
Шаг 1. Сделаем класс
Coffee абстрактным. У нас появилось целых два семейства различных продуктов. У итальянских и американских кофейных напитков по-прежнему есть общий предок, класс
Coffee. Было бы правильно сделать его абстрактным:
public abstract class Coffee {
public void makeCoffee(){
// делаем кофе
}
public void pourIntoCup(){
// наливаем в чашку
}
}
Заодно обрати внимание: метод
grindCoffee() из класса пропал, и дальше в
orderCoffee() его вызова тоже не будет. Это не опечатка, а сознательное упрощение: помол у итальянского и американского кофе свой, так что в общий абстрактный класс его выносить не стали. На UML-диаграмме ниже у
Coffee тоже остались только два метода.
Шаг 2. CoffeeShop и фабричный метод
Шаг 2. Делаем
CoffeeShop абстрактным, с абстрактным методом
createCoffee(CoffeeType type)
public abstract class CoffeeShop {
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = createCoffee(type);
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Вот ваш кофе! Спасибо, приходите еще!");
return coffee;
}
protected abstract Coffee createCoffee(CoffeeType type);
}
Шаги 3 и 4. Итальянская и американская кофейни
Шаг 3. Создадим итальянскую кофейню, класс-потомок абстрактной кофейни. В нем мы реализуем метод
createCoffee(CoffeeType type) с учетом итальянской специфики.
public class ItalianCoffeeShop extends CoffeeShop {
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new ItalianStyleAmericano();
break;
case ESPRESSO:
coffee = new ItalianStyleEspresso();
break;
case CAPPUCCINO:
coffee = new ItalianStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new ItalianStyleCaffeLatte();
break;
}
return coffee;
}
}
Шаг 4. Проделаем то же самое для кофейни в американском стиле
public class AmericanCoffeeShop extends CoffeeShop {
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new AmericanStyleAmericano();
break;
case ESPRESSO:
coffee = new AmericanStyleEspresso();
break;
case CAPPUCCINO:
coffee = new AmericanStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new AmericanStyleCaffeLatte();
break;
}
return coffee;
}
}
Шаг 5. Заказ в обеих кофейнях
Шаг 5. Взглянем на то, как будет выглядеть заказ латте в американском и итальянском стиле:
public class Main {
public static void main(String[] args) {
CoffeeShop italianCoffeeShop = new ItalianCoffeeShop();
italianCoffeeShop.orderCoffee(CoffeeType.CAFFE_LATTE);
CoffeeShop americanCoffeeShop = new AmericanCoffeeShop();
americanCoffeeShop.orderCoffee(CoffeeType.CAFFE_LATTE);
}
}
Поздравляю тебя. Мы только что реализовали шаблон проектирования фабричный метод на примере нашей кофейни.
Как это пишется сегодня: switch-выражения
Обрати внимание на одну деталь: во всех наших фабричных методах повторяется один и тот же скелет. Заводим переменную со значением
null, гоняем
switch, в каждой ветке присваиваем и не забываем
break, в конце возвращаем. Пятнадцать строк ради одного выбора.
С Java 14 у
switch появилась вторая форма, выражение (
JEP 361), и тот же метод сжимается вот до этого:
public class ItalianCoffeeShop extends CoffeeShop {
@Override
public Coffee createCoffee(CoffeeType type) {
return switch (type) {
case AMERICANO -> new ItalianStyleAmericano();
case ESPRESSO -> new ItalianStyleEspresso();
case CAPPUCCINO -> new ItalianStyleCappuccino();
case CAFFE_LATTE -> new ItalianStyleCaffeLatte();
};
}
}
Выигрыш тут не только в длине. Переменной под результат больше нет, а значит негде случайно вернуть
null. Про
break можно забыть: у стрелочных веток нет проваливания. И самое полезное:
switch-выражение по перечислению обязано быть исчерпывающим. Добавишь в
CoffeeType новый вид кофе и забудешь про него в кофейне, код просто не соберется. Старый вариант в такой ситуации спокойно скомпилируется и вернет
null уже во время работы программы.
На сам паттерн это не влияет никак: фабричный метод остается фабричным методом, меняется только то, как написана его начинка. Листинги выше специально оставлены в классическом виде, чтобы синтаксис не отвлекал от сути.
Принцип работы фабричного метода
Теперь рассмотрим подробнее, что же у нас получилось.
На диаграмме ниже собраны получившиеся классы. Зеленые блоки это классы-создатели, голубые это классы-продукты.
![UML-диаграмма кофейни: зеленые классы-создатели CoffeeShop, ItalianCoffeeShop и AmericanCoffeeShop, голубые классы-продукты Coffee и восемь его наследников]()
Какие выводы можно сделать?
- Все продукты это реализации абстрактного класса
Coffee.
- Все создатели это реализации абстрактного класса
CoffeeShop.
- Мы наблюдаем две параллельные иерархии классов:
- Иерархия продуктов. Мы видим итальянских потомков и американских потомков
- Иерархия создателей. Мы видим итальянских потомков и американских потомков
- У суперкласса
CoffeeShop нет информации о том, какая конкретно реализация продукта (Coffee) будет создана.
- Суперкласс
CoffeeShop делегирует создание конкретного продукта своим потомкам.
- Каждый потомок класса
CoffeeShop реализует фабричный метод createCoffee() в соответствии со своей спецификой. Иными словами, внутри реализаций классов-создателей принимается решение о приготовлении конкретного продукта, исходя из специфики класса создателя.
Теперь ты готов к определению паттерна
фабричный метод.
Паттерн фабричный метод определяет интерфейс создания объекта, но позволяет субклассам выбрать класс создаваемого экземпляра. Таким образом, Фабричный метод делегирует операцию создания экземпляра субклассам.
В общем, не столь важно помнить определение, как понимать, как все работает.
Чем фабричный метод отличается от соседних паттернов
Фабричных паттернов несколько, и путают их постоянно. Проще всего запомнить разницу, поставив их рядом:
|
Простая фабрика |
Фабричный метод |
Абстрактная фабрика |
| Кто создает продукт |
Отдельный класс-фабрика |
Наследник класса-создателя, через переопределенный метод |
Отдельный объект-фабрика, но с несколькими методами создания |
| Как выбирается вариант |
По аргументу метода, обычно через switch |
По тому, какой наследник создателя используется |
По тому, какая реализация фабрики передана |
| Что меняется при новом семействе |
Правится switch внутри фабрики |
Добавляется новый наследник создателя, старый код не трогается |
Добавляется новая реализация интерфейса фабрики |
| Сколько продуктов создает |
Один вид продукта |
Один вид продукта |
Целое семейство связанных продуктов |
| Механизм |
Обычный метод |
Наследование и переопределение |
Композиция и делегирование |
Про третий столбец у нас будет
отдельная лекция, а
про первый мы уже говорили в прошлый раз.
Структура фабричного метода
![Схема паттерна: класс Creator с методами factoryMethod и operation, его наследник ConcreteCreator создает ConcreteProduct, наследника класса Product]()
На схеме выше представлена общая структура паттерна фабричный метод.
Что еще здесь важно?
- Класс Creator содержит реализации всех методов, взаимодействующих с продуктами, кроме фабричного метода.
- Абстрактный метод
factoryMethod() должен быть реализован всеми потомками класса Creator.
- Класс
ConcreteCreator реализует метод factoryMethod(), непосредственно производящий продукт.
- Данный класс отвечает за создание конкретных продуктов. Это единственный класс с информацией о создании этих продуктов.
- Все продукты должны реализовывать общий интерфейс, то есть быть потомками общего класса-продукта. Это нужно, чтобы классы, использующие продукты, могли оперировать ими на уровне абстракций, а не конкретных реализаций.
Домашнее задание
Итак, сегодня мы провели довольно большую работу и изучили паттерн проектирования фабричный метод. Самое время закрепить пройденный материал!
Задание 1. Поработать над открытием еще одной кофейни. Она может быть выполнена в английском стиле или испанском. Или даже в стиле космического корабля. Добавим пищевых красителей в кофе, чтоб блестело, и вообще, кофе будет просто космос!
Задание 2. На
прошлой лекции у тебя было задание создать виртуальный суши-бар либо виртуальную пиццерию. Твоя задача: не стоять на месте. Сегодня ты узнал, как с помощью шаблона фабричный метод можно прийти к успеху. Пора воспользоваться этими знаниями и расширить собственный бизнес ;)
Вопросы и ответы
Что такое паттерн фабричный метод?
Это порождающий паттерн, в котором класс-создатель описывает всю работу с продуктом, но само создание продукта выносит в отдельный абстрактный метод. Реализуют этот метод наследники, и каждый возвращает свой вариант продукта. Классическая формулировка звучит так: паттерн определяет интерфейс создания объекта, но позволяет субклассам выбрать класс создаваемого экземпляра.
Чем фабричный метод отличается от простой фабрики?
Местом, где живет создание. В простой фабрике это отдельный класс, объект которого передают снаружи, обычно в конструкторе. В фабричном методе создание живет внутри самого класса-создателя, а разные варианты получаются наследованием. Простая фабрика вообще не входит в классический набор паттернов, это скорее удобный прием.
Чем фабричный метод отличается от абстрактной фабрики?
Количеством продуктов и механизмом. Фабричный метод отвечает за один вид продукта и работает через наследование: наследник создателя переопределяет один метод. Абстрактная фабрика создает целое семейство связанных продуктов и работает через композицию: объект-фабрику передают в клиентский код готовым. Часто абстрактная фабрика внутри себя как раз и состоит из фабричных методов.
Зачем делать класс-создатель абстрактным?
Чтобы никто не мог создать создателя без конкретной реализации фабричного метода. Абстрактный класс задает общий сценарий работы с продуктом, но оставляет дырку в том месте, где нужно решить, какой именно продукт делать. Заполнить эту дырку обязан каждый наследник, иначе код не скомпилируется.
Какие проблемы решает фабричный метод?
Основных две. Первая: клиентский код перестает зависеть от конкретных классов продуктов, он работает только с их общим предком. Вторая: при появлении нового семейства продуктов ничего не надо переписывать, достаточно добавить нового наследника создателя. Старые классы остаются нетронутыми.
Какие минусы у фабричного метода?
Растет число классов. На каждое новое семейство продуктов появляется и новый класс-создатель, а значит две параллельные иерархии, за которыми нужно следить. Если семейств всего одно и новых не предвидится, паттерн только усложнит код. Плюс сам выбор конкретного продукта внутри метода обычно остается условным оператором, от него паттерн не избавляет.
Читайте также
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ