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 o 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 Laminas, 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ę 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ą, dlatego należy nie nadużywać takich obiektów.

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, bo spowoduje powiązanie z konkretnym typem łamiąc ostatnią z zasad SOLID – odwrócenie zależności (dependency inversion). 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, można zyskać 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, odniosę się do zasad SOLID. Niestety reguła otwarte-zamknięte (open-closed) 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 zamknąć tę logikę w fabryce. Wówczas, jedyne co klient wie to jakiego typu obiekt jest zwracany, ale nie jak jest tworzony. 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). Brak powiązania z konkretnym typem zapewnia elastyczność. Dodatkowo fabryka ma tylko jedną odpowiedzialność (single responsibility), którą jest stworzenie innego obiektu.

Przykładowa implementacja w PHP

Poniższa fabryka jest prostą, ale kompletną implementacją. 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.

MealFactory to fabryka, która potrafi stworzyć posiłek. Decyzję o tym, czy powinien on zawierać produkty pochodzenia zwierzęcego podejmuje na podstawie typu posiłku. Na ten moment istnieją dwa rodzaje posiłków: wegańskie i wegetariańskie.

<?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 bez wiedzy, że to znany wzorzec projektowy. Jej użycie bardzo szybko się spłaca i pozwala ustrzec się niepotrzebnej ifologi. Jest też fajną opcją na rozpoczęcie przygody z fabrykam. Pozwala zrozumieć, po co ich używać. Zarówno metoda wytwórcza jak i fabryka abstrakcyjna są zdecydowanie trudniejsze do zaimplementowania.

Uważam ten wzorzec za bardzo przydatny i niejednokrotnie z powodzeniem go używam. To co mnie najbardziej przekonuje to możliwość pozbycia się powtórzonych linijek kodu, w tym instrukcji warunkowych. Staram się tworzyć fabryki tylko tam gdzie ma to sens, ale nawet jeśli użyje jej na wszelki wypadek, to do tej pory nie miałem z tego tytułu nieprzyjemnych skutków.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.