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

Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
Проблема
Представим сервис доставки уведомлений, который умеет отправлять письма:
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 Factory | Factory Method | |
|---|---|---|
| Кто выбирает продукт | Один класс, в рантайме | Подкласс, через переопределение |
| Как добавить новый тип | Дописать ветку if/запись в массив | Добавить новый подкласс |
| Нужна рефлексия/маппинг | Часто да | Нет |
| Количество классов | Меньше | Больше |
Simple Factory проще и требует меньше классов — для небольшого фиксированного набора типов это разумный выбор. Factory Method оправдан, когда список типов растёт регулярно и его должны уметь расширять извне (например, в библиотеке или фреймворке), не трогая код фабрики.
Итог
Фабричный метод не устраняет условную логику полностью, а перемещает её в одно предсказуемое место — иерархию создателей. Это удобно, когда система должна оставаться открытой для новых типов объектов, но закрытой для изменений уже написанного кода.