Перейти к содержимому
Фёдор Башинский

Фабричный метод

5 min read
Обложка статьи «Фабричный метод»

Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.

Проблема

Представим сервис доставки уведомлений, который умеет отправлять письма:

class NotificationService
{
    public function send(string $message): void
    {
        $mailer = new EmailNotification();
        $mailer->send($message);
    }
}

Когда появляется требование отправлять ещё и SMS или push-уведомления, NotificationService начинает разрастаться условиями:

class NotificationService
{
    public function send(string $type, string $message): void
    {
        if ($type === 'email') {
            $notification = new EmailNotification();
        } elseif ($type === 'sms') {
            $notification = new SmsNotification();
        } elseif ($type === 'push') {
            $notification = new PushNotification();
        } else {
            throw new InvalidArgumentException("Unknown type: {$type}");
        }
 
        $notification->send($message);
    }
}

Класс напрямую знает о всех конкретных реализациях и нарушает принцип открытости/закрытости: чтобы добавить новый канал, нужно менять уже работающий код.

Решение

Фабричный метод предлагает вынести создание объекта в отдельный метод, который переопределяется в подклассах. Клиентский код работает с общим интерфейсом и не знает, какая именно реализация будет создана.

Сначала опишем общий интерфейс продукта:

interface Notification
{
    public function send(string $message): void;
}
 
class EmailNotification implements Notification
{
    public function send(string $message): void
    {
        echo "Отправка письма: {$message}" . PHP_EOL;
    }
}
 
class SmsNotification implements Notification
{
    public function send(string $message): void
    {
        echo "Отправка SMS: {$message}" . PHP_EOL;
    }
}
 
class PushNotification implements Notification
{
    public function send(string $message): void
    {
        echo "Отправка push-уведомления: {$message}" . PHP_EOL;
    }
}

Теперь — абстрактный создатель с фабричным методом createNotification(). Он не создаёт объекты сам, а лишь объявляет, что подклассы обязаны это делать:

abstract class NotificationCreator
{
    abstract protected function createNotification(): Notification;
 
    public function send(string $message): void
    {
        $notification = $this->createNotification();
        $notification->send($message);
    }
}

И конкретные создатели, каждый из которых знает, какой продукт создавать:

class EmailNotificationCreator extends NotificationCreator
{
    protected function createNotification(): Notification
    {
        return new EmailNotification();
    }
}
 
class SmsNotificationCreator extends NotificationCreator
{
    protected function createNotification(): Notification
    {
        return new SmsNotification();
    }
}
 
class PushNotificationCreator extends NotificationCreator
{
    protected function createNotification(): Notification
    {
        return new PushNotification();
    }
}

Клиентский код работает только с абстракцией NotificationCreator и не подозревает о конкретных классах:

function notifyUser(NotificationCreator $creator, string $message): void
{
    $creator->send($message);
}
 
notifyUser(new EmailNotificationCreator(), 'Заказ оформлен');
notifyUser(new SmsNotificationCreator(), 'Код подтверждения: 4821');

Чтобы добавить новый канал — например, Telegram — не нужно трогать ни NotificationCreator, ни существующие реализации. Достаточно добавить новый продукт и нового создателя:

class TelegramNotification implements Notification
{
    public function send(string $message): void
    {
        echo "Отправка сообщения в Telegram: {$message}" . PHP_EOL;
    }
}
 
class TelegramNotificationCreator extends NotificationCreator
{
    protected function createNotification(): Notification
    {
        return new TelegramNotification();
    }
}

Из чего состоит паттерн

  • Продукт (Notification) — общий интерфейс объектов, которые создаёт фабрика.
  • Конкретные продукты (EmailNotification, SmsNotification, ...) — реализации интерфейса.
  • Создатель (NotificationCreator) — объявляет фабричный метод и обычно содержит основную бизнес-логику, использующую продукт.
  • Конкретные создатели — переопределяют фабричный метод, возвращая конкретную реализацию продукта.

Когда применять

  • Заранее неизвестно, объекты каких типов и зависимостей понадобятся коду.
  • Нужно дать возможность расширять библиотеку или фреймворк собственными компонентами.
  • Хочется избавить логику от повторяющихся if/switch по типу создаваемого объекта, сосредоточив создание в одном месте.

Плюсы и минусы

Паттерн убирает жёсткую связь между создателем и конкретными продуктами, упрощает добавление новых типов без изменения существующего кода и облегчает тестирование — в тестах можно подменить фабричный метод на возврат мока.

Обратная сторона — рост числа классов: на каждый новый продукт приходится заводить отдельный подкласс создателя. Для одного-двух вариантов это оверинжиниринг; паттерн окупается, когда типов действительно много и они появляются регулярно.

Фабричный метод vs Simple Factory

На практике под «фабричным методом» часто понимают любой код, где создание объекта спрятано за отдельным методом или классом. Например:

class CarEngineFactory implements ICarEngineFactory
{
    protected static $availableEngines = [
        Mercedes::class => MercedesEngine::class,
        Bmw::class => BmwEngine::class,
    ];
 
    public function make(ICar $car): IEngine
    {
        $carClass = get_class($car);
 
        if (!array_key_exists($carClass, self::$availableEngines)) {
            throw new CarEngineNotAvailableException();
        }
 
        $engineClass = self::$availableEngines[$carClass];
 
        return new $engineClass();
    }
}

Это рабочий и очень распространённый код, но с точки зрения GoF это не Factory Method, а Simple Factory: один класс сам решает, что создать, опираясь на таблицу соответствий и рефлексию (get_class(), new $engineClass()). Выбор продукта происходит в рантайме внутри одного метода — по сути это тот же if/switch из раздела «Проблема», только оформленный как поиск по массиву.

Настоящий Factory Method переносит этот выбор на этап компиляции — через переопределение метода в подклассах, без единого условия или рефлексии:

abstract class CarEngineFactory implements ICarEngineFactory
{
    abstract public function make(ICar $car): IEngine;
}
 
class MercedesEngineFactory extends CarEngineFactory
{
    public function make(ICar $car): IEngine
    {
        return new MercedesEngine();
    }
}
 
class BmwEngineFactory extends CarEngineFactory
{
    public function make(ICar $car): IEngine
    {
        return new BmwEngine();
    }
}

Разница на практике:

Simple FactoryFactory Method
Кто выбирает продуктОдин класс, в рантаймеПодкласс, через переопределение
Как добавить новый типДописать ветку if/запись в массивДобавить новый подкласс
Нужна рефлексия/маппингЧасто даНет
Количество классовМеньшеБольше

Simple Factory проще и требует меньше классов — для небольшого фиксированного набора типов это разумный выбор. Factory Method оправдан, когда список типов растёт регулярно и его должны уметь расширять извне (например, в библиотеке или фреймворке), не трогая код фабрики.

Итог

Фабричный метод не устраняет условную логику полностью, а перемещает её в одно предсказуемое место — иерархию создателей. Это удобно, когда система должна оставаться открытой для новых типов объектов, но закрытой для изменений уже написанного кода.