Skip to content

Abstract Factory

The Abstract Factory Pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.

It allows a client to create compatible products that belong to the same family.


Structure

Role Example Responsibility
Abstract Factory NotificationFactory Declares methods for creating each product type
Concrete Factory EmailProvider, SmsProvider Creates a specific family of related products
Abstract Product MessageSender, MessageFormatter Defines the interface for product types
Concrete Product EmailSender, EmailFormatter, SMSSender, SMSFormatter Implements product variants

Steps

  1. Create Abstract Product interfaces
  2. Create Concrete Products that implement these interfaces
  3. Create an Abstract Factory interface
  4. Implement Concrete Factories that produce product families
classDiagram
    class NotificationFactory {
        <<interface>>
        +createSender()
        +createFormatter()
    }

    class EmailNotificationFactory {
        +createSender()
        +createFormatter()
    }

    class SmsNotificationFactory {
        +createSender()
        +createFormatter()
    }

    class MessageSender {
        <<interface>>
        +send(message)
    }

    class MessageFormatter {
        <<interface>>
        +format(message)
    }

    class EmailSender
    class SMSSender
    class EmailFormatter
    class SMSFormatter

    EmailNotificationFactory ..|> NotificationFactory
    SmsNotificationFactory ..|> NotificationFactory

    EmailSender ..|> MessageSender
    SMSSender ..|> MessageSender

    EmailFormatter ..|> MessageFormatter
    SMSFormatter ..|> MessageFormatter

    EmailNotificationFactory --> EmailSender
    EmailNotificationFactory --> EmailFormatter
    SmsNotificationFactory --> SMSSender
    SmsNotificationFactory --> SMSFormatter

The client works only with the factory interface (like NotificationFactory) and product interfaces (like MessageSender, MessageFormatter), making it easy to switch entire product families without changing application code.


Example 1: Notification System

NotificationFactory creates families of:

MessageSender + MessageFormatter

Switching from Email to SMS delivery requires changing only the factory implementation.


Abstract Factory Interface

NotificationFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory;

use DesignPattern\Creational\AbstractFactory\Notification\MessageFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\MessageSender;

interface NotificationFactory
{
    public function createFormatter(): MessageFormatter;

    public function createSender(): MessageSender;
}

Product Interfaces

MessageSender.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Notification;

interface MessageSender
{
    public function send(string $recipient, string $message): void;
}
MessageFormatter.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Notification;

interface MessageFormatter
{
    /**
     * @param array<string, mixed> $data
     */
    public function format(array $data): string;
}

Concrete Factories & Products

EmailNotificationFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory;

use DesignPattern\Creational\AbstractFactory\Notification\MessageFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\MessageSender;
use DesignPattern\Creational\AbstractFactory\Notification\EmailProvider\EmailFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\EmailProvider\EmailSender;

class EmailNotificationFactory implements NotificationFactory
{
    public function createFormatter(): MessageFormatter
    {
        return new EmailFormatter();
    }

    public function createSender(): MessageSender
    {
        return new EmailSender();
    }
}
SmsNotificationFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory;

use DesignPattern\Creational\AbstractFactory\Notification\MessageFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\MessageSender;
use DesignPattern\Creational\AbstractFactory\Notification\SmsProvider\SmsFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\SmsProvider\SmsSender;

class SmsNotificationFactory implements NotificationFactory
{
    public function createFormatter(): MessageFormatter
    {
        return new SmsFormatter();
    }

    public function createSender(): MessageSender
    {
        return new SmsSender();
    }
}

Tests

NotificationTest.php
<?php

declare(strict_types=1);

namespace Tests\Creational\AbstractFactory\Notification;

use DesignPattern\Creational\AbstractFactory\Notification\EmailProvider\EmailFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\SmsProvider\SmsFormatter;
use DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory\EmailNotificationFactory;
use DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory\SmsNotificationFactory;
use DesignPattern\Creational\AbstractFactory\Notification\NotificationFactory\NotificationFactory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(NotificationFactory::class)]
class NotificationTest extends TestCase
{
    /**
     * @return array<int, mixed>
     */
    public static function notificationFactoryProvider(): array
    {
        return [
            [new EmailNotificationFactory()],
            [new SmsNotificationFactory()],
        ];
    }

    public function testEmailFormatter(): void
    {
        $formatter = new EmailFormatter();

        $data = [
            'subject' => 'Hello',
            'body' => 'World',
        ];

        $result = $formatter->format($data);

        self::assertSame("Subject: Hello\n\nWorld", $result);
    }

    public function testSmsFormatter(): void
    {
        $formatter = new SmsFormatter();

        $data = [
            'subject' => 'Hello',
            'body' => 'World',
        ];

        $result = $formatter->format($data);

        self::assertSame('Hello: World', $result);
    }

    #[DataProvider('notificationFactoryProvider')]
    public function testFormatter(NotificationFactory $factory): void
    {
        $formatter = $factory->createFormatter();

        $data = [
            'subject' => 'Order',
            'body' => 'Your order shipped',
        ];

        $result = $formatter->format($data);

        self::assertStringContainsString('Order', $result);
        self::assertStringContainsString('Your order shipped', $result);
    }

    #[DataProvider('notificationFactoryProvider')]
    public function testSender(NotificationFactory $factory): void
    {
        $sender = $factory->createSender();

        self::expectOutputRegex('/Sending (EMAIL|SMS) to/');

        $sender->send('user@example.com', 'Test message');
    }
}

Example 2: UI Theme System

ThemeFactory creates families of UI components:

Button + Checkbox

All components produced by one factory are guaranteed to be visually consistent.


Abstract Factory Interface

ThemeFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory;

use DesignPattern\Creational\AbstractFactory\Theme\Button;
use DesignPattern\Creational\AbstractFactory\Theme\Checkbox;

interface ThemeFactory
{
    public function createButton(): Button;
    public function createCheckbox(): Checkbox;
}

Product Interfaces

Button.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Theme;

interface Button
{
    public function render(): string;
}
Checkbox.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Theme;

interface Checkbox
{
    public function render(): string;

    public function interactWithButton(Button $button): string;
}

Concrete Factories & Products

DarkThemeFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory;

use DesignPattern\Creational\AbstractFactory\Theme\Button;
use DesignPattern\Creational\AbstractFactory\Theme\Checkbox;
use DesignPattern\Creational\AbstractFactory\Theme\DarkTheme\DarkButton;
use DesignPattern\Creational\AbstractFactory\Theme\DarkTheme\DarkCheckbox;

class DarkThemeFactory implements ThemeFactory
{
    public function createButton(): Button
    {
        return new DarkButton();
    }

    public function createCheckbox(): Checkbox
    {
        return new DarkCheckbox();
    }
}
LightThemeFactory.php
<?php

declare(strict_types=1);

namespace DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory;

use DesignPattern\Creational\AbstractFactory\Theme\Button;
use DesignPattern\Creational\AbstractFactory\Theme\Checkbox;
use DesignPattern\Creational\AbstractFactory\Theme\LightTheme\LightButton;
use DesignPattern\Creational\AbstractFactory\Theme\LightTheme\LightCheckbox;

class LightThemeFactory implements ThemeFactory
{
    public function createButton(): Button
    {
        return new LightButton();
    }

    public function createCheckbox(): Checkbox
    {
        return new LightCheckbox();
    }
}

Tests

ThemeTest.php
<?php

declare(strict_types=1);

namespace Tests\Creational\AbstractFactory\Theme;

use DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory\ThemeFactory;
use DesignPattern\Creational\AbstractFactory\Theme\DarkTheme\DarkButton;
use DesignPattern\Creational\AbstractFactory\Theme\DarkTheme\DarkCheckbox;
use DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory\DarkThemeFactory;
use DesignPattern\Creational\AbstractFactory\Theme\ThemeFactory\LightThemeFactory;
use DesignPattern\Creational\AbstractFactory\Theme\LightTheme\LightButton;
use DesignPattern\Creational\AbstractFactory\Theme\LightTheme\LightCheckbox;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(ThemeFactory::class)]
class ThemeTest extends TestCase
{
    /**
     * @return array<int, array{0: ThemeFactory}>
     */
    public static function themeFactoryProvider(): array
    {
        return [
            [new LightThemeFactory()],
            [new DarkThemeFactory()],
        ];
    }

    public function testLightButton(): void
    {
        $button = new LightButton();

        self::assertSame(
            'Rendering a LIGHT themed button.',
            $button->render()
        );
    }

    public function testDarkButton(): void
    {
        $button = new DarkButton();

        self::assertSame(
            'Rendering a DARK themed button.',
            $button->render()
        );
    }

    public function testLightCheckbox(): void
    {
        $checkbox = new LightCheckbox();

        self::assertSame(
            'Rendering a LIGHT themed checkbox.',
            $checkbox->render()
        );
    }

    public function testDarkCheckbox(): void
    {
        $checkbox = new DarkCheckbox();

        self::assertSame(
            'Rendering a DARK themed checkbox.',
            $checkbox->render()
        );
    }

    #[DataProvider('themeFactoryProvider')]
    public function testButton(ThemeFactory $factory): void
    {
        $button = $factory->createButton();

        $result = $button->render();

        self::assertStringContainsString('button', $result);
    }

    #[DataProvider('themeFactoryProvider')]
    public function testCheckbox(ThemeFactory $factory): void
    {
        $checkbox = $factory->createCheckbox();

        $result = $checkbox->render();

        self::assertStringContainsString('checkbox', $result);
    }

    #[DataProvider('themeFactoryProvider')]
    public function testThemeConsistency(ThemeFactory $factory): void
    {
        $button = $factory->createButton();
        $checkbox = $factory->createCheckbox();

        $buttonRender = $button->render();
        $checkboxRender = $checkbox->render();

        if (str_contains($buttonRender, 'LIGHT')) {
            self::assertStringContainsString('LIGHT', $checkboxRender);
        }

        if (str_contains($buttonRender, 'DARK')) {
            self::assertStringContainsString('DARK', $checkboxRender);
        }
    }
}