Simple Factory (Prosta Fabryka)

simple factory uml

Opis

Wzorzec projektowy Simple Factory należy do grupy wzorców konstrukcyjnych. Fabryka jest odpowiedzialna za stworzenie obiektu jednej z kilku powiązanych klas na podstawie przekazanych parametrów. Ich powiązanie odbywa się przez implementację wspólnego interfejsu lub rzadziej przez rozszerzenie klasy abstrakcyjnej.

Niektórzy mogą się przyczepić, że prosta fabryka nie jest pełnoprawnym wzorcem projektowym, a w wielu materiałach poświęconym tej tematyce go nie zobaczycie. Ja jednak uważam, że jest fajnym wstępem do pozostałych typów fabryk i należy o nim wspomnieć.

Warto dodać, że można spotkać przypadki, gdzie klasy których odpowiedzialnością jest stworzenie innego obiektu nazywane są fabrykami, nawet jeśli nie ma tam mowy o wspólnym interfejsie. O ile z samą definicją nie do końca jest to zgodne, o tyle dodanie do nazwy klasy sufiksu Factory jest dość czytelnym zabiegiem. Przykładem mogą być chociażby klasy tworzące kontrolery i serwisy w Zend Framework 3, które „prawdziwymi” fabrykami raczej nie są, ale ułatwiają konfigurację i zarządzanie zależnościami.

Mi również zdarza się używać tego typu zabiegów, szczególnie dla klas, których inicjalizacja nie jest trywialna lub w enkapsulacji logiki tworzenia obiektu widzę jakieś korzyści. Jedyne odstępstwo od fabryki jest wtedy takie, że zwracany jest konkretny obiekt, a nie interfejs. Może się jednak okazać, że taka implementacja będzie skutkowała zbędną warstwą pośrednią.

Problem i rozwiązanie

Gdzieś w kodzie musi dojść do konkretyzacji klasy. W PHP w tym celu używa się słowa kluczowego new. Problem w tym, że nie w każdym miejscu stworzenie obiektu będzie odpowiednie, gdyż spowoduje powiązanie z konkretnym typem łamiąc ostatnią z zasad SOLID – odwrócenie zależności. Taka implementacja jest wówczas trudna do przetestowania i rozszerzania.

Tutaj do akcji wchodzi fabryka. Wydzielając kod odpowiedzialny za tworzenie obiektów o wspólnym interfejsie do osobnej klasy zyskać można głównie korzyści – o czym piszę w kolejnej sekcji.

Plusy i minusy

Simple Factory w zasadzie nie ma wielu minusów, więc od nich zacznę. Jak zawsze trzeba odnieść się do zasad SOLID. Niestety reguła otwarte-zamknięte nie jest tutaj spełniona. Nie obejdzie się bez ingerencji w metodę tworzącą obiekt w momencie kiedy dojdzie nowa klasa implementująca interfejs. Mimo wszystko chciałbym zaznaczyć, że gdyby fabryki nie było, tylko jej kod byłby powielany w wielu miejscach to modyfikacja byłaby jeszcze trudniejsza.

I tyle jeśli chodzi o wady. Ewentualnie fabryka stworzona na siłę może stać się zbędną warstwą łamiąc zasadę YAGNI (You Aren’t Gonna Need It). Ale znowu… to nie wada wzorca, a złego użycia. Poza tym ta reguła ma dla mnie sens wtedy jeśli pisanie kodu na zaś jest czasochłonne lub wprowadza niepotrzebną złożoność. Oba te argumenty nijak mają się do prostej fabryki.

Zalet znajdzie się trochę więcej. Operacja tworzenia obiektu zazwyczaj występuje w wielu miejscach. Zamiast powielać ją w kodzie należy zaenkapsulować tę logikę w fabryce. Wówczas jedyne co klient wie to jakiego typu obiekt jest zwracany, ale już jak jest tworzony nie. Oczywiście dopóki jest to prosty switch to w zasadzie ta zaleta nic nie wnosi. Bardziej będzie widoczna w trudniejszych przypadkach.

Dzięki fabryce kod jest reużywalny i łatwiejszy w utrzymaniu, tym samym żyje w zgodzie z zasadą DRY (Don’t Repeat Yourself). Natomiast brak powiązania z konkretnym typem zapewnia elastyczność. Dodatkowo fabryka ma tylko jedną odpowiedzialność (SRP), którą jest stworzenie innego obiektu.

Przykładowa implementacja w PHP

Poniższa fabryka jest prostą, ale kompletną implementacją i dla większości przypadków okaże się wystarczająca. To z czym można się spotkać to oznaczenie metody create() jako statyczna. Ja tam gdzie nie muszę to staticów nie używam stąd wolę klasyczne podejście.

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory;

interface MealInterface
{
    public function containsAnimalProducts(): bool;
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory;

final class VeganMeal implements MealInterface
{
    public function containsAnimalProducts(): bool
    {
        return false;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory;

final class VegetarianMeal implements MealInterface
{
    public function containsAnimalProducts(): bool
    {
        return true;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory;

final class MealType
{
    public const TYPE_VEGETARIAN = 'vegetarian';
    public const TYPE_VEGAN = 'vegan';

    public function __construct(
        public readonly string $name
    ) {}
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory;

use InvalidArgumentException;

final class MealFactory
{
    public function create(MealType $mealType): MealInterface
    {
        return match ($mealType->getName()) {
            MealType::TYPE_VEGETARIAN => new VegetarianMeal(),
            MealType::TYPE_VEGAN => new VeganMeal(),
            default => throw new InvalidArgumentException('The type of meal does not exist'),
        };
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\SimpleFactory\Test;

use DesignPatterns\Creational\SimpleFactory\VegetarianMeal;
use DesignPatterns\Creational\SimpleFactory\VeganMeal;
use DesignPatterns\Creational\SimpleFactory\MealFactory;
use DesignPatterns\Creational\SimpleFactory\MealInterface;
use DesignPatterns\Creational\SimpleFactory\MealType;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

final class MealFactoryTest extends TestCase
{
    public function testCanCreateVegetarianMeal(): void
    {
        $mealType = new MealType(MealType::TYPE_VEGETARIAN);

        $meal = (new MealFactory())->create($mealType);

        self::assertInstanceOf(MealInterface::class, $meal);
        self::assertInstanceOf(VegetarianMeal::class, $meal);
    }

    public function testCanCreateVeganMeal(): void
    {
        $mealType = new MealType(MealType::TYPE_VEGAN);

        $meal = (new MealFactory())->create($mealType);

        self::assertInstanceOf(MealInterface::class, $meal);
        self::assertInstanceOf(VeganMeal::class, $meal);
    }

    public function testCannotCreateMealOfNonExistentType(): void
    {
        $mealType = new MealType('Paleo');

        self::expectException(InvalidArgumentException::class);

        (new MealFactory())->create($mealType);
    }
}

Simple Factory – podsumowanie

Prosta fabryka to świetne i banalne rozwiązanie. Pewnie niektórzy używają tego mechanizmu nie wiedząc, że to znany wzorzec projektowy. Bardzo szybko jej użycie się spłaca i pozwala ustrzec się niepotrzebnej ifologi. Jest też fajną opcją na rozpoczęcie przygody z fabrykami i zrozumienie po co ich używać, gdyż zarówno metoda wytwórcza jak i fabryka abstrakcyjna są zdecydowanie trudniejsze do zaimplementowania.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.