
Привет! Меня зовут Александр, я разработчик в Битрикс24.
Заметил такую особенность во многих учебных статьях и туториалах: в популярных объяснениях паттернов часто не хватает оговорки, что ради упрощения объяснения в примере нарушены принципы SOLID.
Почему так: многие из примеров решают задачу обучения, а не задачу проектирования архитектуры. То есть примеры часто короткие и удобные для объяснения, но иногда вместе с этим они теряют важные свойства самого паттерна.
Особенно часто это происходит с Factory Method. В этой статье мы разберём несколько популярных примеров этого паттерна, посмотрим, где именно возникает проблема, и обсудим альтернативы в реальных проектах.
Чтобы дальше не путаться в терминах, сначала коротко зафиксируем, что такое принцип OCP: программные сущности должны быть открыты для расширения, но закрыты для изменения. Проще говоря, если завтра в системе появляется новый тип объекта, нам хотелось бы добавить новый класс и подключить его к системе, а не переписывать уже работающий код в нескольких местах.
Примеры для разбора
Ниже несколько материалов, где фабрика или Factory Method объясняются через выбор реализации по строке, switch, match или похожую центральную развилку. Такие примеры полезны как материал для разбора: внешне они выглядят как паттерн, но при добавлении нового типа часто требуют менять уже существующую фабрику.
JavaRush: Паттерн проектирования Factory — пример, с которого начался этот разбор.
Quipoin: Factory Method Design Pattern in Java with Real Example — пример NotificationFactory с выбором через switch.
MagicBell: Notification System Design — пример NotificationFactoryImpl с switch(type) и заявленным OCP.
JavaGuides: Java Factory Pattern with Real-World Examples — показано нарушение OCP через if/else, а затем фабрика со switch.
MakeDev: Фабричный метод на PHP — русскоязычный пример с FactoryAbstract::create($type) и switch.
PHPLex: Factory Pattern — PHP-пример с enum и match.
ndup.io: Factory Pattern in Python — пример NotificationFactory.create_notifier() через if/elif.
LIG: PHP Factory Pattern — пример, где проблема switch обсуждается, но похожая развилка остается в фабриках.
Christopher Okhravi: Factory Method Pattern — полезно как контрастное объяснение разницы между Simple Factory и Factory Method.
Все примеры выглядят довольно разумно. Проблема становится заметна не в первой версии кода, а позже, когда система начинает расти.
Важно: проблема не в фабричном методе как паттерне, а в примерах, где фабрикой называют обычный create($type) с набором if/elseif. Такая фабрика нарушает принцип Open/Closed Principle (OCP), потому что к новый тип объекта требует возвращаться в уже существующую фабрику и изменять её.
Пример с нарушением SOLID (OCP)
class ProductFactory { public function create(string $type): Product { if ($type === 'car') { return new Car(); } elseif ($type === 'bike') { return new Bike(); } throw new InvalidArgumentException('Unknown type'); } }
В чем тут проблема? Допустим, у нас появился еще один продукт:
class Truck implements Product {}
Тогда чтобы его добавить в фабрику нам нужно будет залезть в этот метод и добавить в него такой код:
elseif ($type === 'truck') { return new Truck(); }
И это нарушает принцип открытости/закрытости (OCP).
Проблема в примере выше в том, что под «фабрикой» здесь фактически скрывается большой оператор выбора. Если заменить if/elseif на switch или match, архитектурная проблема не исчезнет. Изменится только синтаксис.
Пример NotificationFactory
Пример с автомобилями хорошо показывает нарушение OCP, но выглядит немного искусственно. В реальных проектах фабрики чаще встречаются при работе с интеграциями, платёжными системами, обработчиками событий.
Следующий пример с нарушением чуть ближе к реальным задачам:
interface Notification { public function send(): void; }
class NotificationFactory { public function create(string $type): Notification { if ($type === 'email') { return new EmailNotification(); } if ($type === 'sms') { return new SmsNotification(); } if ($type === 'push') { return new PushNotification(); } throw new InvalidArgumentException('Unknown type'); } }
Если добавился, например, Telegram, то привет новый if:
if ($type === 'telegram') { return new TelegramNotification(); }
Здесь мы упираемся в главную проблему такой реализации: фабрика знает обо всех возможных типах уведомлений. Email, SMS, Push, Telegram — все эти варианты собираются в одном месте.
Пока типов мало, это кажется удобным. Но с ростом фабрика превращается в список условий: она уже не просто создаёт объекты, а становится центральной точкой, которую приходится редактировать при каждом новом бизнес-требовании.
Что с этим можно сделать? Для начала добавим интерфейс для уведомлений:
interface Notification { public function send(): void; }
Далее опишем абстрактный класс для фабричного метода (в идеале еще и интерфейс добавить)
abstract class NotificationService { abstract public function create(): Notification; public function process(): void { $notification = $this->create(); $notification->send(); } }
Далее унаследуем каждый сервис от этого класса
class EmailService extends NotificationService { public function create(): Notification { return new EmailNotification(); } }
class SmsService extends NotificationService { public function create(): Notification { return new SmsNotification(); } }
Добавился Telegram? Не проблема:
class TelegramService extends NotificationService { public function create(): Notification { return new TelegramNotification(); } }
Обратите внимание, что при появлении Telegram нам больше не нужно открывать и изменять EmailService или SmsService. Мы просто добавляем новый класс TelegramService. Старые классы при этом остаются неизменными. Именно в этом практический смысл OCP: новая возможность появляется через расширение системы, а не через постоянное редактирование уже работающего кода.
Тут встает закономерный вопрос: В чём смысл этих фабрик, если в конечном итоге придётся делать выбор нужной фабрики:
$type = $_POST['CLIENT_NOTIFICATION_SERVICE'] ?? ''; $service = null; if ($type === 'email') { $service = new EmailService(); } elseif ($type === 'sms') { $service = new SmsService(); } elseif ($type === 'telegram') { $service = new TelegramService(); } else { throw new InvalidArgumentException('Unknown type'); } $service->process();
Это справедливый вопрос, потому что выбор реализации действительно никуда не исчезает. Если пользователь передал строку email, sms или telegram, приложение все равно должно понять, какой сервис использовать.
Но важно, где именно происходит этот выбор. Если держать его внутри фабрики, её приходится менять при каждом новом типе уведомлений. Вместо этого можно вынести выбор в отдельное место сборки приложения.
Вот один из простых вариантов: сделать NotificationCenter, который будет работать как реестр доступных сервисов.
class NotificationCenter { /** @var array<string, NotificationService> */ private array $services = []; public function register(string $type, NotificationService $service): void { $this->services[$type] = $service; } public function send(string $type): void { if (!isset($this->services[$type])) { throw new InvalidArgumentException("Unknown type: $type"); } $this->services[$type]->process(); } }
Тогда наш вышеприведённый код будет выглядеть примерно так:
$notificationCenter = new NotificationCenter(); $notificationCenter->register('email', new EmailService()); $notificationCenter->register('sms', new SmsService()); $notificationCenter->register('telegram', new TelegramService()); $type = $_POST['CLIENT_NOTIFICATION_SERVICE'] ?? ''; $notificationCenter->send($type);
Такой подход часто называют реестром объектов: мы заранее регистрируем доступные реализации, а потом выбираем нужную по ключу. Главное отличие от исходной фабрики в том, что логика отправки уведомлений больше не смешана со списком всех возможных типов. Новый канал уведомлений добавляется через новый сервис и регистрацию, а не через правку большого метода create().
Выбор реализации все равно где-то должен произойти. Вопрос не в том, чтобы магически избавиться от выбора, а в том, чтобы вынести его из бизнес-кода в место сборки приложения: конфигурацию, DI-контейнер или registry. Тогда добавление нового типа уведомлений будет требовать добавления нового класса и регистрации в одном предсказуемом месте, а не правки уже существующей фабрики с бизнес-логикой.
При этом не стоит превращать OCP в религию. Если вариантов два, они редко меняются, а фабрика находится в одном месте и не расползается по проекту, простой match/if может быть нормальным решением. Проблема начинается, когда список типов расширяется регулярно, а фабрика становится точкой, которую приходится править при каждом новом бизнес-сценарии.
То есть важен не формальный запрет на if, switch или match. Важен вопрос: как часто этот код будет меняться и насколько безопасно его менять. То есть важно не то, что вы выбрали, а пониманий последствий этого выбора.
Если фабрика обслуживает стабильный набор вариантов, простое условие может быть нормальным решением. Если же новые типы появляются регулярно, то центральная фабрика быстро превращается в узкое место архитектуры.
Почему проблема выходит за рамки учебных статей
Это важно, потому что плохие примеры не остаются только в одной статье. На них учатся люди, их пересказывают на собеседованиях, копируют в рабочий код, а теперь ещё и получают от ИИ по запросу.
LLM-модели часто воспроизводят не самый архитектурно точный вариант, а самый узнаваемый и распространённый. Если в обучающих материалах фабрикой постоянно называют метод create($type) с набором if/elseif, то похожий код легко получить как «пример паттерна Factory Method».
Например, можно дать такой промт:
Напиши на PHP пример паттерна Factory Method для создания уведомлений: email, sms и push. Пользователь должен передавать строковый тип уведомления, а фабрика должна возвращать нужный объект.
С большой вероятностью ответ будет выглядеть примерно так:
class NotificationFactory { public function create(string $type): Notification { return match ($type) { 'email' => new EmailNotification(), 'sms' => new SmsNotification(), 'push' => new PushNotification(), default => throw new InvalidArgumentException('Unknown notification type'), }; } }
На первый взгляд код выглядит нормально: он короткий, понятный и даже использует match. Но архитектурная проблема никуда не делась. Если, например, появится Telegram, нам снова придется открыть NotificationFactory и добавить новую ветку. Значит, класс закрыт для расширения только на словах, а на практике открыт для постоянных изменений.
Чтобы получить более осмысленный пример, промт лучше формулировать сразу с архитектурным ограничением:
Напиши на PHP пример создания уведомлений без нарушения OCP. Новые типы уведомлений должны добавляться через новые классы и регистрацию в контейнере или registry, без изменения существующей фабрики.
Такой промт не гарантирует идеальный ответ, но хотя бы направляет ИИ в сторону нужной архитектурной идеи: новый тип добавляется через новый класс и регистрацию, а не через правку центрального match.
Как проблема становится заметна в реальном проекте
Отдельная проблема появляется, когда пример из статьи пытаются перенести в реальный проект. В учебном коде объект обычно создается просто через new EmailNotification(). Но в рабочем приложении почти любой сервис зависит от других компонентов.
В реальных проектах уведомления почти всегда имеют зависимости. Например, email-уведомлению может понадобиться mailer и шаблонизатор:
class EmailService extends NotificationService { public function __construct( private Mailer $mailer, private TemplateRenderer $renderer, ) {} public function create(): Notification { return new EmailNotification($this->mailer, $this->renderer); } }
Из-за таких зависимостей подход «просто создать класс по строке» быстро превращается в комок ручной сборки объектов. Во фреймворках Symfony/Laravel это обычно решается через DI-контейнер: tagged services, service providers, container binding или конфигурационный map. Можно сделать совсем просто: сложить в какой-нибудь конфигурационный файл массив, а в него добавлять классы, после чего вызывать $array['sms']->process(). Такой подход имеет ряд минусов, но для быстрого решения вполне годится.
Что использовать в итоге
Иногда if, switch или match — нормальное и понятное решение, отказываться от них не стоит.
Проблема начинается не там, где в коде появился match, а там, где этот match приходится менять при каждом новом сценарии. Если система растет, появляются новые типы уведомлений, новые зависимости и новые правила, стоит задуматься о более расширяемой архитектуре.
Паттерны проектирования полезны не потому, что позволяют писать единственно правильный код. Они полезны, когда помогают подготовить систему к изменениям. Поэтому любой пример фабрики стоит оценивать не по тому, насколько он короткий, а по тому, что произойдет, когда в систему придёт новый тип объекта.
Комментарии (12)

japanxt
29.06.2026 17:35Если так хочется упороться за SOLID в фабрике, то можно внутри фабрики вызвать цепочку обязанностей и та (ее звенья) уже сама решит какой продукт вернуть.

Deosis
29.06.2026 17:35Вместо этого можно вынести выбор в отдельное место сборки приложения.
Например, в отдельный класс. И назвать его фабрикой.

IlyaSlayer
29.06.2026 17:35Один хер, будешь ты менять класс где описываются зависимости, или менять класс провайдер фактори где описываются через свитч - это все изменение уже законченного класса
В теории, ничего нарушать не будет, если сделать атрибутами с именем типа, и реализовывая новый тип, ничего кроме указания атрибута не нужно будет .
p.s. а ваще солид уже давно залэпа нафиг никому не нужная кроме как на собесах
Tishka17
Но ведь Factory и Factory Method - это два разных паттерна
infinity92 Автор
Верно. В статье говоря о «фабрике» имеется ввиду фабричный метод. Но некоторое будет справедливо и для абстрактной фабрики
Tishka17
Я может невнимательно читал, но не вижу в статье фабричных методов. Везде отдельный класс-фабрика
Tishka17
Sorry, перечитал внимательно, фабричные методы на месте, но как-то впепремешку с фабриками. Вы там ещё и интерфейс для фабричных методов предлагаете и промпт у вас по запросу фабричного метода абстрактную фабрику выдал.
infinity92 Автор
А что вы считаете интерфейсом абстрактной фабрики?
Tishka17
Интерфейс абстрактной фабрики в вашем случае - требование наличие метода
create. Он имеет смысл, потому что мы можем сказать "вот сюда передайте любой объект с методом create". А как вы собираетесь описывать интерфейс для фабричного метода у меня вызывает вопрос, ведь в основном фабричный метод используется внутри того же класса (см. паттерн шаблонный метод)infinity92 Автор
Если я верно понял, то вас смущает формулировка "Далее опишем абстрактный класс для фабричного метода (в идеале еще и интерфейс добавить)", и далее пример. И вам не понятно зачем тут интерфейс?
Tishka17
Да, я не понимаю как вы собираетесь использовать интерфейс при реализации паттерна фабричный метод
infinity92 Автор
В данном примере интерфейс предпологался не для метода `create`, а для метода `process`. Но это не относится к самому паттерну по этому добавлять его в статью не стал.