Abstract Factory (Fabryka Abstrakcyjna)

abstract factory uml

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. OK, ale problem ten rozwiązują wszystkie typy fabryk. Natomiast Fabryka Abstrakcyjna jest odpowiedzią na jeszcze jedną potrzebę, mianowicie 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ę więc o trochę ifologii i prawdopodobnie sprawdzanie typu klasy. 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ę Open/Closed. Dorzucam też swój post do tej puli i opowiadam się za pierwszą opcją, gdyż po stronie klienta jak najbardziej przy rozszerzaniu nic nie trzeba 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ż SRP (Single Responsibility Principle) i DIP (Dependency Inversion Principle). 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 sporo. Mimo wszystko taki podział wydaje się czytelny nawet jeśli klasy będą bardziej rozbudowane, niż poniższe, które posiadają tylko po jednej losowej metodzie.

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.

<?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(): bool
    {
        return true;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\AbstractFactory;

final class VegetarianBreakfast implements BreakfastInterface
{
    public function shouldAddVitaminB12Supplement(): bool
    {
        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(): bool
    {
        return true;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\AbstractFactory;

final class VegetarianDinner implements DinnerInterface
{
    public function canBePackedInGlassContainer(): bool
    {
        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).

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.

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.