Opis
Wzorzec projektowy Abstract Factory należy do grupy wzorców konstrukcyjnych. Zadaniem Fabryki Abstrakcyjnej jest dostarczenie interfejsu do tworzenia obiektów jednej rodziny bez określania konkretnych klas. Skoro jest tutaj mowa o liczbie mnogiej to należy zauważyć, że w fabrykach zazwyczaj istnieje kilka metod kreacyjnych – dokładnie tyle z ilu obiektów składa się rodzina. Funkcje te są po prostu Metodami Wytwórczymi, a wzorzec ten często nazywany jest fabryką fabryk.
Problem i rozwiązanie
Łatwo wyobrazić sobie sytuację, w której zgodnie z nowymi wymaganiami dochodzi kolejny obiekt danego typu. Na przykład: kolacja – nowy posiłek. W przypadku braku fabryki, trzeba będzie przegrzebać się przez wszystkie miejsca, w których znajduje się logika tworzenia tych obiektów i obsłużyć nowy. No dobra, ale problem ten rozwiązują wszystkie typy fabryk. Natomiast Fabryka Abstrakcyjna jest odpowiedzią na jeszcze jedną potrzebę. Grupuje całą rodzinę obiektów.
Zostając w temacie diety, przychodzi moment, w którym trzeba stworzyć jadłospis na cały dzień. Oczywiście dla diet wegańskiej i wegetariańskiej mamy pewne restrykcje przy wyborze posiłków. Tutaj do gry wchodzą dedykowane fabryki dla całej rodziny obiektów. Do klasy klienta wstrzykiwana za pomocą kompozycji jest konkretna fabryka realizująca interfejs tej abstrakcyjnej. Taka operacja daje pewność, że wykonane wewnątrz akcje (w tym przypadku metody wytwórcze) zadziałają dla konkretnej grupy obiektów. Weganie i wegetarianie otrzymają więc właściwe jadłospisy.
Bez tego wzorca projektowego sytuacja okaże się bardziej skomplikowana. Logika oprze się o trochę ifologii i prawdopodobnie sprawdzanie typu obiektu. Tym samym abstrakcja, która została wprowadzona nie ma żadnej wartości. Zupełnie niepotrzebny interfejs, a kod dużo cięższy w testowaniu i rozszerzaniu.
Plusy i minusy
Zastanawiam się, dlaczego w wielu artykułach poświęconym abstrakcyjnej wersji fabryki istnieje niejasność, czy owy mechanizm wspiera, czy łamie zasadę owarte-zamknięte (open-closed). Dorzucam też swój post do tej puli i opowiadam się za pierwszą opcją, gdyż po stronie klienta jak najbardziej przy rozszerzaniu nie trzeba nic zmieniać. Dodam tylko, że sam proces rozszerzania fabryki abstrakcyjnej nie zawsze jest przyjemny, ale ten wzorzec projektowy po prostu należy do grupy tych bardziej skomplikowanych.
Jeśli chodzi o solidne zasady to Abstract Factory realizuje też regułę pojedynczej odpowiedzialności (single responsibility) i regułę odwrócenia zależności (dependency inversion). Dodatkowo zapewnia elastyczność na poziomie braku związania z konkretnymi typami. Swój mechanizm skupia na kreacji obiektów różnych rodzin spełniających ten sam interfejs, który może być użyty w wielu miejscach.
Abstract Factory ma te same zalety co Factory Method, ale obie te implementacje mają nieco inne zastosowanie. Częściej mimo wszystko znajdzie się miejsce na tę drugą, która ewentualnie w przyszłości może ewoluować w tę pierwszą.
Przykładowa implementacja w PHP
Oczywiście, żeby nie zaciemniać istoty Abstract Factory przykładowa implementacja jest najprostsza. Zauważcie, że już przy dwóch rodzinach produktów i tak klas jest całkiem sporo. Mimo wszystko, taki podział wydaje się czytelny nawet jeśli klasy będą bardziej rozbudowane. Te posiadają tylko po jednej losowej metodzie.
Fabryka abstrakcyjna definiuje kontrakt dla wytwarzania śniadań i obiadów. Metody createBreakfast() i createDinner() są tak naprawdę metodami wytwórczymi, stąd też częste określenie fabryki abstrakcyjnej jako fabryki fabryk – o czym pisałem we wstępie. Dzięki temu w łatwy sposób można tworzyć posiłki w wersji wegetariańskiej i wegańskiej w zależności od zdefiniowanego rodzaju.
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
interface BreakfastInterface
{
public function shouldAddVitaminB12Supplement(): bool;
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VeganBreakfast implements BreakfastInterface
{
public function shouldAddVitaminB12Supplement(): true
{
return true;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VegetarianBreakfast implements BreakfastInterface
{
public function shouldAddVitaminB12Supplement(): false
{
return false;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
interface DinnerInterface
{
public function canBePackedInGlassContainer(): bool;
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VeganDinner implements DinnerInterface
{
public function canBePackedInGlassContainer(): true
{
return true;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VegetarianDinner implements DinnerInterface
{
public function canBePackedInGlassContainer(): true
{
return true;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
abstract class AbstractMealFactory
{
abstract public function createBreakfast(): BreakfastInterface;
abstract public function createDinner(): DinnerInterface;
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VeganMealFactory extends AbstractMealFactory
{
public function createBreakfast(): BreakfastInterface
{
return new VeganBreakfast();
}
public function createDinner(): DinnerInterface
{
return new VeganDinner();
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory;
final class VegetarianMealFactory extends AbstractMealFactory
{
public function createBreakfast(): BreakfastInterface
{
return new VegetarianBreakfast();
}
public function createDinner(): DinnerInterface
{
return new VegetarianDinner();
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory\Test;
use DesignPatterns\Creational\AbstractFactory\BreakfastInterface;
use DesignPatterns\Creational\AbstractFactory\DinnerInterface;
use DesignPatterns\Creational\AbstractFactory\VeganBreakfast;
use DesignPatterns\Creational\AbstractFactory\VeganDinner;
use DesignPatterns\Creational\AbstractFactory\VeganMealFactory;
use PHPUnit\Framework\TestCase;
final class VeganMealFactoryTest extends TestCase
{
public function testCanCreateVeganBreakfast(): void
{
$meal = (new VeganMealFactory())->createBreakfast();
self::assertInstanceOf(VeganBreakfast::class, $meal);
self::assertInstanceOf(BreakfastInterface::class, $meal);
}
public function testCanCreateVeganDinner(): void
{
$meal = (new VeganMealFactory())->createDinner();
self::assertInstanceOf(VeganDinner::class, $meal);
self::assertInstanceOf(DinnerInterface::class, $meal);
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory\Test;
use DesignPatterns\Creational\AbstractFactory\BreakfastInterface;
use DesignPatterns\Creational\AbstractFactory\DinnerInterface;
use DesignPatterns\Creational\AbstractFactory\VegetarianBreakfast;
use DesignPatterns\Creational\AbstractFactory\VegetarianDinner;
use DesignPatterns\Creational\AbstractFactory\VegetarianMealFactory;
use PHPUnit\Framework\TestCase;
final class VegetarianMealFactoryTest extends TestCase
{
public function testCanCreateVegetarianBreakfast(): void
{
$meal = (new VegetarianMealFactory())->createBreakfast();
self::assertInstanceOf(VegetarianBreakfast::class, $meal);
self::assertInstanceOf(BreakfastInterface::class, $meal);
}
public function testCanCreateVegetarianDinner(): void
{
$meal = (new VegetarianMealFactory())->createDinner();
self::assertInstanceOf(VegetarianDinner::class, $meal);
self::assertInstanceOf(DinnerInterface::class, $meal);
}
}
Abstract Factory – podsumowanie
Fabryka Abstrakcyjna to najbardziej skomplikowana wersja wytwórni. Wykorzystuje takie mechanizmy jak dziedziczenie i kompozycja, a w dodatku korzysta z enkapsulacji i polimorfizmu. Patrząc na wszystkie terminy, którymi rzuciłem powyżej, wzorzec ten to kwintesencja OOP (Object-Oriented Programming).
Wzorzec Abstract Factory w PHP nie jest tak często implementowany jak inne typy fabryk, a to dlatego że problem, który rozwiązuje, występuje zdecydowanie rzadziej. W praktyce zaimplementowany we właściwym miejscu jak najbardziej daje wiele korzyści. Mechanizm ten daje także pole pod inne wzorce. Można go zaimplementować jako Singleton albo połączyć z Builderem. Zalecam jednak uważnie korzystać z dobrodziejstw jakie niesie, żeby nie przysporzyć sobie problemów poprzez złe wykorzystanie.
Nie do końca rozumiem jak korzystać z takiej fabryki. Tak czy inaczej powinien wtedy być if lub case, bo na jakiej podstawie program będzie wiedział którą fabrykę zaimplementować, bo to powinno zależeć od typu danych? W przypadku SimpleFactory to jest jasne, a tu nie mam pojęcia jak z tego korzystać w realnych projektach. Czy można prosić o jakiś bardziej złożony przykład?
Gdzieś zawsze będziesz musiał podjąć decyzję, która implementacja ma być użyta. Może będzie to if, może case, a może po prostu będziesz akurat działał na jednym z tych obiektów albo gdzieś w konfiguracji wskażesz, który wstrzyknąć za pomocą dependency injection. To co jest tutaj istotne, to fakt że tworzysz abstrakcję i nie bazujesz na konkretnej implementacji. Możesz więc w trakcie działania programu podmieniać obiekty spełniające ten typ. Jak dojdzie kolejny nowy to po prostu go stworzysz i gdzieś użyjesz – nie będziesz musiał przerabiać istniejącego już kodu.