Opis
Wzorzec projektowy Factory Method należy do grupy wzorców konstrukcyjnych. Metoda Wytwórcza, jak każda z fabryk, jest odpowiedzialna za dostarczenie interfejsu do tworzenia obiektów. W tym wypadku dostarczany jest interfejs, ale już tworzenie konkretnego obiektu reprezentującego wspólny typ ma miejsce w podklasie.
Charakterystyczne dla tego wzorca projektowego jest to, że struktura klas i interfejsów produktu (czyli części wytwarzanej przez fabrykę) jest tożsama ze strukturą klas i interfejsów fabryk. Tym samym dla każdego powiązanego typu istnieje dokładnie jedna fabryka.
Problem i rozwiązanie
Metoda Wytwórcza rozwiązuje ten sam problem co Prosta Fabryka, ale we właściwy sposób, bo w tym celu wykorzystuje mechanizm programowania obiektowego – polimorfizm. Jeśli obiekt zostanie stworzony w złym miejscu to szybko okaże się, że występuje silne powiązanie z konkretnym typem przez co kod staje się ciężki w testowaniu i rozszerzaniu. Skoro gdzieś do konkretyzacji i tak musi dojść, to w części przypadków najbardziej odpowiednim do tego miejscem okaże się fabryka. Przy okazji bardziej skomplikowane kreacje zostaną zamknięte w fabrykach i będą mogły zostać ponownie użyte.

Plusy i minusy
Metoda Wytwórcza nie ma żadnych wad. No dobra – na siłę doszukałem się jednej. Rzeczywiście, jeżeli klas produktów będzie dużo to liczba fabryk także spuchnie, przez co złożoność rośnie. Ciężko mi się wypowiedzieć, bo nie miałem takiego przypadku. Myślę, że to rzadkość, bo przecież ile może być obiektów spełniających wspólny interfejs. Z drugiej strony, ta sama logika bez metody wytwórczej byłaby pewnie jeszcze mniej czytelna i trudniejsza w zarządzaniu. Lepiej mieć 50 czytelnych klas, niż jednego nieczytelnego switcha z 50 przypadkami.
Factory Method niesie ze sobą same korzyści. Żyje w zgodzie z pierwszymi dwoma zasadami SOLID, czyli pojedyncza odpowiedzialność (single responsibility) i otwarte-zamknięte (open-closed). Dobrze enkapsuluje logikę tworzenia obiektu oraz pozwala na jej wielokrotne użycie bez potrzeby duplikacji kodu. Dzięki temu, że działa na abstrakcji zapewnia dużą elastyczność.
Metoda Fabrykująca jest po prostu lepszą implementacją od Prostej Fabryki. Zawiera w sobie wszystkie jej dobrodziejstwa i dodatkowo przezwycięża jej największy minus, czyli łamanie zasady otwarte-zamknięte.
Przykładowa implementacja w PHP
Zaprezentowany mechanizm metody wytwórczej jest uniwersalny. Zazwyczaj w początkowej fazie dokładnie tak wygląda. Czasem jednak budowanie obiektów jest trochę bardziej skomplikowane i wymaga przekazania zależności do konstruktora. Myślę, że taka implementacja jest właściwa żeby zrozumieć, na czym polega ten wzorzec projektowy. Kolejne rozszerzenia powinny być już łatwe do dorzucenia.
MealFactoryInterface
definiuje konktrakt dla fabryki, która potrafi stworzyć posiłek. Każdy z typów posiłków posiada swoją własną wytwórnie, która spełnia ten interfejs. Na ten moment istnieją dwa rodzaje posiłków: wegańskie i wegetariańskie.
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
interface MealInterface
{
public function containsAnimalProducts(): bool;
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
final class VeganMeal implements MealInterface
{
public function containsAnimalProducts(): false
{
return false;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
final class VegetarianMeal implements MealInterface
{
public function containsAnimalProducts(): true
{
return true;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
interface MealFactoryInterface
{
public function createMeal(): MealInterface;
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
final class VeganMealFactory implements MealFactoryInterface
{
public function createMeal(): MealInterface
{
return new VeganMeal();
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
final class VegetarianMealFactory implements MealFactoryInterface
{
public function createMeal(): MealInterface
{
return new VegetarianMeal();
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod\Test;
use DesignPatterns\Creational\FactoryMethod\VeganMeal;
use DesignPatterns\Creational\FactoryMethod\VeganMealFactory;
use DesignPatterns\Creational\FactoryMethod\MealInterface;
use PHPUnit\Framework\TestCase;
final class VeganMealFactoryTest extends TestCase
{
public function testCanCreateVeganMeal(): void
{
$meal = (new VeganMealFactory())->createMeal();
self::assertInstanceOf(MealInterface::class, $meal);
self::assertInstanceOf(VeganMeal::class, $meal);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod\Test;
use DesignPatterns\Creational\FactoryMethod\VegetarianMeal;
use DesignPatterns\Creational\FactoryMethod\VegetarianMealFactory;
use DesignPatterns\Creational\FactoryMethod\MealInterface;
use PHPUnit\Framework\TestCase;
final class VegetarianMealFactoryTest extends TestCase
{
public function testCanCreateVegetarianMeal(): void
{
$meal = (new VegetarianMealFactory())->createMeal();
self::assertInstanceOf(MealInterface::class, $meal);
self::assertInstanceOf(VegetarianMeal::class, $meal);
}
}
Factory Method – podsumowanie
Metoda fabrykująca to esencja wzorców projektowych – posiada wiele zalet i praktycznie żadnych wad. Tak jak wspomniałem jest sensowniejszym mechanizmem, niż Simple Factory. Z tym, że ta druga jest dużo łatwiejsza w implementacji jeśli chodzi o zastany, słabo przemyślany kod. Przy tworzeniu od zera zazwyczaj szedłbym w tę pierwszą.
Spotkałem się z opinią, że przewagą Metody Wytwórczej jest możliwość wyniesienia wyżej i wyabstrachowania jakiegoś wspólnego zachowania. Albo czegoś nie rozumiem albo to wymyślona przewaga, bo w obu przypadkach można ten mechanizm zaimplementować. Dla mnie jedyna różnica, ale bardzo znacząca jest taka, że zamiast jednego wielkiego switcha i jednej metody, mamy dla każdej klasy dedykowaną wytwórnię.
Wzorzec Factory Method w PHP jest z powodzeniem wykorzystywany. Nie wymaga dużego poświęcenia, a jego użycie niesie ze sobą same plusy. Wspiera dobre praktyki i otwiera pole do użycia kolejnych wzorców projektowych.
Zastanawiam się czy metoda AbstractMealFactory::isYummy() nie łamie SRP.
Odpowiedzialnością klas factory jest tworzenie obiektów a tutaj mamy dodatkowo jakąś logikę nie związaną z samym procesem tworzenia.
Czy zamiast AbstractMealFactory nie lepszy byłby MealFactoryInterface?
Tak jak na przykład tutaj: https://designpatternsphp.readthedocs.io/en/latest/Creational/FactoryMethod/README.html
Hmm. Dałeś mi do myślenia. Chyba faktycznie nie najlepiej pokazałem w tym przykładzie, o co tak naprawdę chodzi. Sama metoda wytwórcza poza metodą do tworzenia obiektu, może (choć nie musi) mieć inne operacje. W wielu konceptualnych przykładach znajdziesz metody typu:
someOperation()
,otherMethod()
itp. Co do samego SRP – wszystko zależy jakie odpowiedzialności przyjmą te metody.Przyznam, że często zdarza mi się zaimplementować ją w taki sposób jak podesłałeś, czyli w postaci interfejsu i jedyną odpowiedzialnością jest tworzenie obiektu. Tutaj posiadam jednak dodatkową metodę, która dostarcza domyślną implementację, więc zamknąłem to w klasę abstrakcyjną. Taka metoda nie jest błędem, a w niektórych źródłach znajdziesz nawet, że jest to esencja tego wzorca i na tym on polega. Cóż, tak czy inaczej wydaje mi się, że metoda
isYummy()
nie jest najlepszą formą takiej dodatkowej operacji. A nawet nie mam pomysłu dla tego przykładu, co mogłoby nią być. Poza tym, tak naprawdę ta implementacja bardziej przypomina metodę wytwórczą połączoną z metodą szablonową.Podsumowując – cenna uwaga. Dziękuję. Implementacja zmieniona według sugestii. Jest to czytelniejsze rozwiązanie, a
isYummy()
faktycznie przeniósłbym jako odpowiedzialność innej klasy, ale nie jest to tutaj konieczne do pokazania.Wow, dzięki za tak rozbudowaną odpowiedź 😀
Tak jak napisałeś, w dużej części materiałów dokładane są metody pokroju „someOperation()”.
Ale na moje wyczucie ich miejsce powinno być w innej klasie.
Tak czy siak tworzysz ŚWIETNE materiały (wszystkie, serio) za co bardzo dziękuję.
W końcu się zabrałem za pełnoprawne poznawanie wzorców i dałeś mi kopa do nauki 🙂
Bardzo mnie to cieszy, dzięki. ?
Hej,
czego mi tutaj brakuję, to wytłumaczenie przejścia z Simple Factory, do Factory Method.
Mam tutaj na myśli logikę wybierania właściwej fabryki.
W Simple według Twojego przykładu jest to switch w klasie fabryki, który inicjalizuje odpowiednią klasę posiłku według parametru w metodzie create.
Zatem w jaki sposób mając jakąś logikę w klasie wykonującej (np. controller), przyjmując jakieś parametry np. preferencje użytkownika co do posiłku z formularza, wybrać właściwą fabrykę?
Czy teraz aby wybrać odpowiednią fabrykę, switch który w Simple znajdował się w samej fabryce trzeba przenieść do kodu wykonującego i na podstawie danych wybrać odpowiednią fabrykę, która zainicjuje odpowiednią klasę?
Jakoś nie potrafię zrozumieć tej koncepcji.
Mógłbyś pomóc?
Staram się każdy wpis o wzorcu traktować w miarę niezależnie – stąd też odwołania między fabrykami brakuje. Wiem też, że może brakuje właśnie przypadku użycia, bo poza testem nie pokazuje jak użyć tego w kodzie klienckim.
W każdym razie istnieje kilka opcji. Jedna jest taka jaką opisałeś, jeśli możesz podjąć taką decyzję niezależnie (chociaż nie robiłbym tego bezpośrednio w kontrolerze tylko w innej klasie, bo pewnie ta logika będzie użyta jeszcze w innych miejscach). Druga to połączenie tego z wzorcem strategii. Czasem możesz też podjąć tę decyzję wcześniej i wstrzyknąć już konkretny rodzaj fabryki. Sposobów jest pewnie jeszcze więcej, jak zwykle zależy od przypadku i potrzeby.
Żeby zilustrować na bardziej przydatnym przykładzie – wyobraźmy sobie np 50 różnych importów plików txt do zaciągania, każdy o własnej strukturze. Potrzebujemy do tego mechanizm który „opisze” pliki importu, nastąpi walidacja importowanych danych i po imporcie zapisze się z tej operacji LOG no i dane w odpowiednich tabelach bazy danych, korzystając z tego samego mechanizmu zaimplementowanego w interfejsie i klasie abstrakcyjnej.
Chodzi o ujęcie sposobu przetwarzania w instrukcję dla programu.