JavaRush /Java блог /Random UA /Патерни проектування: FactoryMethod
Professor Hans Noodles
41 рівень

Патерни проектування: FactoryMethod

Стаття з групи Random UA
Вітання! Сьогодні ми продовжимо вивчати патерни проектування та поговоримо про фабричний метод (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. Закажем наш кофе
         */
        SimpleAmericanCoffeeFactory americanCoffeeFactory = new SimpleAmericanCoffeeFactory();
        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. Всі продукти повинні реалізовувати спільний інтерфейс – бути нащадками загального класу продукту. Це потрібно, щоб класи, які використовують продукти, могли оперувати ними лише на рівні абстракцій, а чи не конкретних реалізацій.

Домашнє завдання

Отже, сьогодні ми провели досить велику роботу та вивчабо патерн проектування фабричний метод. Саме час закріпити пройдений матеріал! Завдання 1. Попрацювати над відкриттям ще однієї кав'ярні. Вона може бути виконана в англійському або іспанському стилі. Або навіть у стилі космічного корабля. Додамо харчових барвників у каву, щоб блищало, і взагалі, кава буде просто космос! Завдання 2. На минулій лекції ти мав завдання створити віртуальний суші-бар або віртуальну піцерію. Твоє завдання – не стояти на місці. Сьогодні ти дізнався, як за допомогою шаблону фабричний метод можна досягти успіху. Настав час скористатися цими знаннями і розширити власний бізнес ;)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ