CodeGym /Blog Java /Random-PL /Wzorce projektowe: metoda fabryczna
Autor
Andrey Gorkovenko
Frontend Engineer at NFON AG

Wzorce projektowe: metoda fabryczna

Opublikowano w grupie Random-PL
Cześć! Dzisiaj będziemy kontynuować badanie wzorców projektowych i omówimy wzorzec metody fabrycznej. Wzorce projektowe: Metoda fabryczna - 1 Dowiesz się, co to jest i do jakich zadań nadaje się ten wzór. Rozważymy ten wzorzec projektowy w praktyce i przestudiujemy jego strukturę. Aby wszystko było jasne, musisz zrozumieć następujące tematy:
  1. Dziedziczenie w Javie.
  2. Abstrakcyjne metody i klasy w Javie

Jaki problem rozwiązuje metoda fabryczna?

We wszystkich wzorcach projektowania fabryk występują dwa typy uczestników: twórcy (same fabryki) i produkty (obiekty stworzone przez fabryki). Wyobraźmy sobie następującą sytuację: mamy fabrykę produkującą samochody marki CodeGym. Wie, jak tworzyć modele samochodów z różnymi typami nadwozi:
  • sedany
  • kombi
  • coupe
Nasza firma prosperowała tak dobrze, że pewnego pięknego dnia przejęliśmy innego producenta samochodów — OneAuto. Jako rozsądni właściciele firm nie chcemy stracić żadnych klientów OneAuto, dlatego też stoimy przed zadaniem restrukturyzacji produkcji, tak abyśmy mogli produkować:
  • Sedany CodeGym
  • Kombi CodeGym
  • Coupe CodeGym
  • sedany OneAuto
  • Kombi OneAuto
  • Coupe OneAuto
Jak widać zamiast jednej grupy produktów mamy teraz dwie, różniące się pewnymi szczegółami. Wzorzec projektowy metody fabrycznej jest używany, gdy musimy stworzyć różne grupy produktów, z których każda ma określone cechy. Zasadę przewodnią tego wzorca rozważymy w praktyce, stopniowo przechodząc od prostych do złożonych, na przykładzie naszej kawiarni, którą stworzyliśmy na jednej z poprzednich lekcji .

Trochę o wzorze fabrycznym

Przypomnę, że wcześniej zbudowaliśmy małą wirtualną kawiarnię. Przy pomocy prostej fabryki nauczyliśmy się tworzyć różne rodzaje kawy. Dzisiaj przerobimy ten przykład. Przypomnijmy sobie, jak wyglądała nasza kawiarnia, z jej prostą fabryką. Umówiliśmy się na kawę:

public class Coffee {
    public void grindCoffee(){
        // Grind the coffee
    }
    public void makeCoffee(){
        // Brew the coffee
    }
    public void pourIntoCup(){
        // Pour into a cup
    }
}
Oraz kilka klas potomnych odpowiadających konkretnym rodzajom kawy, które nasza fabryka mogłaby produkować:

public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
Stworzyliśmy wyliczenie, aby ułatwić składanie zamówień:

public enum CoffeeType {
    ESPRESSO,
    AMERICANO,
    CAFFE_LATTE,
    CAPPUCCINO
}
Sama fabryka kawy wyglądała tak:

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;
    }
}
I na koniec sama kawiarnia wyglądała tak:

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("Here's your coffee! Thanks! Come again!");
        return coffee;
    }
}

Modernizacja prostej fabryki

Nasza kawiarnia działa bardzo dobrze. Do tego stopnia, że ​​zastanawiamy się nad rozszerzeniem. Chcemy otworzyć kilka nowych lokalizacji. Jesteśmy odważni i przedsiębiorczy, więc nie będziemy wykręcać nudnych kawiarni. Chcemy, aby każdy sklep miał wyjątkowy charakter. W związku z tym na początek otworzymy dwie lokalizacje: jedną włoską i jedną amerykańską. Zmiany te dotkną nie tylko wystrój wnętrz, ale także oferowane napoje:
  • we włoskiej kawiarni będziemy używać wyłącznie włoskich marek kaw, ze specjalnym mieleniem i paleniem.
  • lokal w Ameryce będzie miał większe porcje, a do każdego zamówienia będziemy podawać pianki.
Jedyne, co pozostaje niezmienne, to nasz model biznesowy, który sprawdził się doskonale. Jeśli chodzi o kod, tak się dzieje. Mieliśmy 4 klasy odpowiadające naszym produktom:

public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
Ale teraz będziemy mieli 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 {}
Ponieważ zależy nam na zachowaniu dotychczasowego modelu biznesowego, zależy nam na tym, aby orderCoffee(CoffeeType type)metoda podlegała jak najmniejszej liczbie zmian. Spójrz na to:

public Coffee orderCoffee(CoffeeType type) {
    Coffee coffee = coffeeFactory.createCoffee(type);
    coffee.grindCoffee();
    coffee.makeCoffee();
    coffee.pourIntoCup();

    System.out.println("Here's your coffee! Thanks! Come again!");
    return coffee;
}
Jakie mamy opcje? Cóż, wiemy już, jak napisać fabrykę, prawda? Najprostszą rzeczą, która od razu przychodzi na myśl, jest napisanie dwóch podobnych fabryk, a następnie przekazanie pożądanej implementacji konstruktorowi naszej kawiarni. W ten sposób klasa kawiarni nie ulegnie zmianie. Najpierw musimy utworzyć nową klasę fabryczną, sprawić, by odziedziczyła naszą prostą fabrykę, a następnie nadpisać metodę createCoffee(CoffeeType type). Napiszmy fabryki do tworzenia kawy po włosku i po amerykańsku:

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;
    }

}
Teraz możemy przekazać pożądaną implementację fabryczną do CoffeeShop. Zobaczmy, jak wyglądałby kod do zamawiania kawy w różnych kawiarniach. Na przykład cappuccino w stylu włoskim i amerykańskim:

public class Main {
    public static void main(String[] args) {
        /*
            Order an Italian-style cappuccino:
            1. Create a factory for making Italian coffee
            2. Create a new coffee shop, passing the Italian coffee factory to it through the constructor
            3. Order our coffee
         */
        SimpleItalianCoffeeFactory italianCoffeeFactory = new SimpleItalianCoffeeFactory();
        CoffeeShop italianCoffeeShop = new CoffeeShop(italianCoffeeFactory);
        italianCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
        
        
         /*
            Order an American-style cappuccino
            1. Create a factory for making American coffee
            2. Create a new coffee shop, passing the American coffee factory to it through the constructor
            3. Order our coffee
         */
        SimpleAmericanCoffeeFactory americanCoffeeFactory = new SimpleAmericanCoffeeFactory();
        CoffeeShop americanCoffeeShop = new CoffeeShop(americanCoffeeFactory);
        americanCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
    }
}
Stworzyliśmy dwie różne kawiarnie, przekazując każdemu pożądaną fabrykę. Z jednej strony osiągnęliśmy nasz cel, ale z drugiej strony... Przedsiębiorcom to jakoś nie pasuje... Zastanówmy się, co jest nie tak. Po pierwsze, obfitość fabryk. Co? Teraz dla każdej nowej lokalizacji mamy stworzyć własną fabrykę, a do tego przy tworzeniu kawiarni przekazać odpowiednią fabrykę konstruktorowi? Po drugie, to wciąż prosta fabryka. Tylko lekko zmodernizowany. Ale jesteśmy tutaj, aby nauczyć się nowego wzorca. Po trzecie, czy nie jest możliwe inne podejście? Byłoby wspaniale, gdybyśmy mogli umieścić wszystkie kwestie związane z przygotowaniem kawy w jednymCoffeeShopklasy poprzez powiązanie procesów tworzenia kawy i obsługi zamówień, przy jednoczesnym zachowaniu wystarczającej elastyczności do przygotowania różnych stylów kawy. Odpowiedź brzmi: tak, możemy. Nazywa się to wzorcem projektowym metody fabrycznej.

Od prostej fabryki do metody fabrycznej

Aby rozwiązać zadanie tak efektywnie, jak to możliwe:
  1. Zwracamy createCoffee(CoffeeType type)metodę do CoffeeShopklasy.
  2. Uczynimy tę metodę abstrakcyjną.
  3. Sama klasa CoffeeShopstanie się abstrakcyjna.
  4. Klasa CoffeeShopbędzie miała zajęcia podrzędne.
Tak przyjacielu. Włoska kawiarnia to nic innego jak potomek klasy CoffeeShop, która realizuje createCoffee(CoffeeType type)metodę zgodnie z najlepszymi tradycjami włoskich baristów. Teraz krok po kroku. Krok 1. Utwórz Coffeeklasę abstrakcyjną. Mamy dwie całe rodziny różnych produktów. Mimo to włoska i amerykańska kawa mają wspólnego przodka — klasę Coffee. Właściwe byłoby uczynienie go abstrakcyjnym:

public abstract class Coffee {
    public void makeCoffee(){
        // Brew the coffee
    }
    public void pourIntoCup(){
        // Pour into a cup
    }
}
Krok 2. Utwórz streszczenie metodą CoffeeShopabstrakcyjnącreateCoffee(CoffeeType type)

public abstract class CoffeeShop {

    public Coffee orderCoffee(CoffeeType type) {
        Coffee coffee = createCoffee(type);

        coffee.makeCoffee();
        coffee.pourIntoCup();

        System.out.println("Here's your coffee! Thanks! Come again!");
        return coffee;
    }

    protected abstract Coffee createCoffee(CoffeeType type);
}
Krok 3. Stwórz włoską kawiarnię, która jest potomkiem abstrakcyjnej kawiarni. Realizujemy createCoffee(CoffeeType type)w nim metodę uwzględniającą specyfikę włoskich receptur.

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;
    }
}
Krok 4. To samo robimy dla kawiarni w stylu amerykańskim

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;
    }
}
Krok 5. Sprawdź, jak będą wyglądać amerykańskie i włoskie latte:

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);
    }
}
Gratulacje. Właśnie zaimplementowaliśmy wzorzec projektowy metody fabrycznej na przykładzie naszej kawiarni.

Zasada metod fabrycznych

Rozważmy teraz bardziej szczegółowo, co mamy. Poniższy diagram przedstawia wynikowe klasy. Zielone bloki to klasy twórców, a niebieskie to klasy produktów. Wzorce projektowe: Metoda fabryczna - 2Jakie wnioski możemy wyciągnąć?
  1. Wszystkie produkty są implementacjami klasy abstrakcyjnej Coffee.
  2. Wszyscy twórcy są implementacjami klasy abstrakcyjnej CoffeeShop.
  3. Widzimy dwie równoległe hierarchie klas:
    • Hierarchia produktów. Widzimy potomków Włochów i potomków Amerykanów
    • Hierarchia twórców. Widzimy potomków Włochów i potomków Amerykanów
  4. Nadklasa CoffeeShopnie ma informacji o tym, który konkretny produkt ( Coffee) zostanie utworzony.
  5. Nadklasa CoffeeShopdeleguje tworzenie określonego produktu swoim potomkom.
  6. Każdy potomek klasy CoffeeShopimplementuje createCoffee()metodę fabryczną zgodnie z własną specyfiką. Innymi słowy, implementacje klas producentów przygotowują określone produkty w oparciu o specyfikę klasy producentów.
Jesteś teraz gotowy do zdefiniowania wzorca metody fabrycznej . Wzorzec metody fabrycznej definiuje interfejs do tworzenia obiektu, ale pozwala podklasom na wybranie klasy tworzonego obiektu. Zatem metoda fabryczna deleguje tworzenie instancji do podklas. Ogólnie rzecz biorąc, zapamiętanie definicji nie jest tak ważne, jak zrozumienie, jak to wszystko działa.

Struktura metody fabrycznej

Wzorce projektowe: Metoda fabryczna - 3Powyższy diagram przedstawia ogólną strukturę wzorca metody fabrycznej. Co jeszcze jest tu ważne?
  1. Klasa Creator implementuje wszystkie metody, które wchodzą w interakcje z produktami, z wyjątkiem metody fabrycznej.
  2. Metoda abstrakcyjna factoryMethod()musi być zaimplementowana przez wszystkich potomków klasy Creator.
  3. Klasa ConcreteCreatorimplementuje factoryMethod()metodę, która bezpośrednio tworzy produkt.
  4. Ta klasa odpowiada za tworzenie określonych produktów. To jedyne zajęcia z informacjami na temat tworzenia tych produktów.
  5. Wszystkie produkty muszą implementować wspólny interfejs, tj. muszą być potomkami wspólnej klasy produktów. Jest to konieczne, aby klasy korzystające z produktów mogły na nich działać jako abstrakcje, a nie konkretne implementacje.

Praca domowa

Dzisiaj wykonaliśmy sporo pracy i przestudiowaliśmy wzorzec projektowy metody fabrycznej. Czas wzmocnić materiał! Ćwiczenie 1. Wykonaj pracę, aby otworzyć kolejną kawiarnię. Może to być kawiarnia w stylu angielskim lub hiszpańskim. Lub nawet w stylu statku kosmicznego. Dodaj barwnik spożywczy do kawy, aby się świeciła, a Twoja kawa będzie po prostu nie z tego świata! Ćwiczenie 2. Na ostatniej lekcji miałeś ćwiczenie, w którym stworzyłeś wirtualny bar sushi lub wirtualną pizzerię. Teraz twoim ćwiczeniem jest nie stać w miejscu. Dzisiaj nauczyłeś się, jak wykorzystać wzorzec metody fabrycznej na swoją korzyść. Czas wykorzystać tę wiedzę i rozwinąć własny biznes ;)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION