Привет! Сегодня мы продолжим изучать паттерны проектирования и поговорим о фабричном методе (FactoryMethod).  Паттерны проектирования: FactoryMethod - 1Ты узнаешь, что это такое и для решения каких задач подходит данный шаблон. Мы рассмотрим этот паттерн проектирования на практике и изучим его структуру. Чтобы все изложенное было тебе понятно, необходимо разбираться в следующих темах:
  1. Наследование в Java.
  2. Абстрактные методы и классы в 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 ItalianStyleCappucino extends Coffee {}
public class ItalianStyleCaffeLatte extends Coffee {}
public class ItalianStyleEspresso extends Coffee {}

public class AmericanStyleAmericano extends Coffee {}
public class AmericanStyleCappucino 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. Закажем наш кофе
         */
        SimpleItalianCoffeeFactory americanCoffeeFactory = new SimpleItalianCoffeeFactory();
        CoffeeShop americanCoffeeShop = new CoffeeShop(americanCoffeeFactory);
        americanCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
    }
}
Мы создали две различные кофейни, передав в каждую нужную фабрику. С одной стороны, мы достигли поставленной задачи, но с другой стороны... Что-то скребет неуемную душу предпринимателя… Давай разбираться, что не так. Во-первых, обилие фабрик. Это что, каждый раз теперь под новую точку свою фабрику создавать и вдобавок следить за тем, чтобы при создании кофейни в конструктор передавалась нужная фабрика? Во-вторых, это все еще простая фабрика. Просто немного модернизированная. Мы тут все-таки новый паттерн изучаем. В-третьих, а что, нельзя что ли по-другому? Вот было бы классно, если бы мы могли локализовать все вопросы по приготовлению кофе внутри класса CoffeeShop, связав процессы по созданию кофе и обслуживанию заказа, но при этом сохранив достаточную гибкость, чтобы делать кофе в различных стилях. Ответ — да, можно. Это называется шаблон проектирования фабричный метод.

От простой фабрики к фабричному методу

Чтобы решить поставленную задачу максимально эффективно, мы:
  1. Вернем метод createCoffee(CoffeeType type) в класс CoffeeShop.
  2. Данный метод сделаем абстрактным.
  3. Сам класс CoffeeShop станет абстрактным.
  4. У класса CoffeeShop появятся наследники.
Да, друг. Итальянская кофейня, это ничто иное, как наследник класса CoffeeShop, реализующий метод createCoffee(CoffeeType type) в соответствии с лучшими традициями итальянских бариста. Итак, по порядку. Шаг 1. Сделаем класс Coffee абстрактным. У нас появилось целых два семейства различных продуктов. У итальянских и американских кофейных напитков по-прежнему есть общий предок — класс Coffee. Было бы правильно сделать его абстрактным:
public abstract class Coffee {
    public void makeCoffee(){
        // делаем кофе
    }
    public void pourIntoCup(){
        // наливаем в чашку
    }
}
Шаг 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. Создадим итальянскую кофейню, класс-потомок абстрактной кофейни. В нем мы реализуем метод 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. Взглянем на то, как будет выглядеть заказ латте в американском и итальянском стиле:
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);
    }
}
Поздравляю тебя. Мы только что реализовали шаблон проектирования фабричный метод на примере нашей кофейни.

Принцип работы фабричного метода

Теперь рассмотрим подробнее, что же у нас получилось. На диаграмме ниже — получившиеся классы. Зеленые блоки — классы создатели, голубые — классы продукты.  Паттерны проектирования: FactoryMethod - 2Какие выводы можно сделать?
  1. Все продукты — реализации абстрактного класса Coffee.
  2. Все создатели — реализации абстрактного класса CoffeeShop.
  3. Мы наблюдаем две параллельные иерархии классов:
    • Иерархия продуктов. Мы видим итальянских потомков и американских потомков
    • Иерархия создателей. Мы видим итальянских потомков и американских потомков
  4. У суперкласса CoffeeShop нет информации о том, какая конкретно реализация продукта (Coffee) будет создана.
  5. Суперкласс CoffeeShop делегирует создание конкретного продукта своим потомкам.
  6. Каждый потомок класса CoffeeShop реализует фабричный метод createCoffee() в соответствии со своей спецификой. Иными словами, внутри реализаций классов-создателей принимается решение о приготовлении конкретного продукта, исходя из специфики класса создателя.
Теперь ты готов к определению паттерна фабричный метод. Паттерн фабричный метод определяет интерфейс создания объекта, но позволяет субклассам выбрать класс создаваемого экземпляра. Таким образом, Фабричный метод делегирует операцию создания экземпляра субклассам. В общем, не столь важно помнить определение, как понимать, как все работает.

Структура фабричного метода

 Паттерны проектирования: FactoryMethod - 3На схеме выше представлена общая структура паттерна фабричный метод. Что еще здесь важно?
  1. Класс Creator содержит реализации всех методов, взаимодействующих с продуктами, кроме фабричного метода.
  2. Абстрактный метод factoryMethod() должен быть реализован всеми потомками класса Creator.
  3. Класс ConcreteCreator реализует метод factoryMethod(), непосредственно производящий продукт.
  4. Данный класс отвечает за создание конкретных продуктов. Это единственный класс с информацией о создании этих продуктов.
  5. Все продукты должны реализовывать общий интерфейс — быть потомками общего класса-продукта. Это нужно, чтобы классы, использующие продукты, могли оперировать ими на уровне абстракций, а не конкретных реализаций.
 Паттерны проектирования: FactoryMethod - 4

Домашнее задание

Итак, сегодня мы провели довольно большую работу и изучили паттерн проектирования фабричный метод. Самое время закрепить пройденный материал! Задание 1. Поработать над открытием еще одной кофейни. Она может быть выполнена в английском стиле или испанском. Или даже в стиле космического корабля. Добавим пищевых красителей в кофе, чтоб блестело, и вообще, кофе будет просто космос! Задание 2. На прошлой лекции у тебя было задание создать виртуальный суши-бар либо виртуальную пиццерию. Твоя задача — не стоять на месте. Сегодня ты узнал, как с помощью шаблона фабричный метод можно придти к успеху. Пора воспользоваться этими знаниями и расширить собственный бизнес ;)