Предметно-ориентированное проектирование (Domain-driven design) - Паттерн Фабрика в PHP
В Domain-driven проектирование мы стремимся к тому, чтобы наша модель домена была прямо пуленепробиваемой. В некоторых случаях необходимо обеспечить соблюдение некоторых бизнес-правил при воплощении нового объекта в жизнь. Если конструкция слишком сложна или просто не может быть реализована самим объектом, тогда вы должны переместить конструкцию объекта в выделенный класс: фабрику.
Приведу краткий пример. Допустим, у нас есть значимый объект TimeSpan. Наша первоначальная реализация TimeSpan может выглядеть так:
class TimeSpan { /** @var \DateTimeImmutable **/ private $from; /** @var \DateTimeImmutable **/ private $until; public function __construct(\DateTimeImmutable $from, \DateTimeImmutable $until) { if ( $from >= $until ) { throw new \InvalidArgumentException('Invalid time span.'); } $this->from = $from; $this->until = $until; } // Some other useful stuff goes in here... }
Допустим, мы не хотим, чтобы интервал времени был неограниченным, поэтому зададим максимум, который может быть задан через конфигурацию. Вот как это выглядит:
class TimeSpanConfiguration { /** @var \DateInterval **/ private $maxTimeSpan; public function __construct(\DateInterval $maxTimeSpan) { $this->maxTimeSpan = $maxTimeSpan; } }
Итак, у нас есть бизнес-правило, которое гласит: не может быть никакого промежутка времени больше максимума, что мы задали. Как же это обеспечить? Создавать экземпляры TimeSpan мы можем как хотим, ведь ничто никогда так и не реализует такую конфигурацию. Но, к счастью, есть множество решений этой проблемы. Я хочу показать вам фабрику:
timeSpanConfiguration = $timeSpanConfiguration; } public function createTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan { // We just ask the configuration if the given from-until time span is valid. // That way we don't need any getters on the configuration. Neat. if ( !$this->timeSpanConfiguration->isValidTimeSpanFromUntil($form, $until) ) { throw new \DomainException('This time span is too long!'); } return new TimeSpan($from, $until); }}
Теперь при создании нового экземпляра TimeSpan мы просто используем TimeSpanFactory, вместо конструктора TimeSpan. Таким образом, мы всегда получаем интервал времени, который не превышает настроенный максимум.
Теперь вы можете спросить: Ну и что?, я ведь все еще могу создать недопустимый интервал времени при использовании конструктора. И да, ты прав! И это может обернуться проблемой в зависимости от команды разработчиков. Если у вас достаточно небольшая команда разработчиков, вы можете просто определиться, что всегда используете TimeSpanFactory вместо конструктора напрямую. Но есть решение, которое заставит вас использовать паттерн фабрика и это Reflection. Использование данного подхода может кое что поломать, но я все равно покажу вам;)
Во-первых, сделайте конструктор TimeSpan private:
class TimeSpan { /** @var \DateTimeImmutable **/ private $from; /** @var \DateTimeImmutable **/ private $until; private function __construct(\DateTimeImmutable $from, \DateTimeImmutable $until) { if ( $from >= $until ) { throw new \InvalidArgumentException('Invalid time span.'); } $this->from = $from; $this->until = $until; } // Some other useful stuff goes in here... }
Теперь вы не сможете создать экземпляр TimeSpan используя оператор new. PHP выдаст фатальную ошибку, когда вы попытаетесь сделать это. Хорошо, но если мы не можем создать его таким образом, то как же тогда наш TimeSpanFactory может создать его? Reflection спешит на помощь! Посмотрим на нашу новую реализацию TimeSpanFactory:
timeSpanConfiguration = $timeSpanConfiguration; } public function createTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan { // We just ask the configuration if the given from-until time span is valid. // That way we don't need any getters on the configuration. Neat. if ( !$this->timeSpanConfiguration->isValidTimeSpanFromUntil($form, $until) ) { throw new \DomainException('This time span is too long!'); } return $this->constructTimeSpan($from, $until); } private function constructTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan { $class = new ReflectionClass(TimeSpan::class); $constructor = $class->getConstructor(); $constructor->setAccessible(true); $timeSpan = $class->newInstanceWithoutConstructor(); $constructor->invoke($timeSpan, $from, $until); return $timeSpan; }}// Usage: $factory = new TimeSpanFactory(new TimeSpanConfiguration(new \DateInterval('PT2D')));$timeSpan = $factory->constructTimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 18:00:00'));// Fails due too to long time span $timeSpan = $factory->constructTimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 23:00:00'));// Also failes, but due to private constructor ;) $timeSpan = new TimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 23:00:00'));
Погодите...
Я согласен, но пусть лучше у меня будет пуленепробиваемая модель домена, а вероятность того, что кто-то создаст недопустимый интервал времени и нарушит бизнес-правило не такая уж и большая. Это может быть младший разработчик, который не знает, что вам следует использовать TimeSpanFactory, или даже старший, который просто забыл о Factory.
Но, как я уже сказал, это зависит от вашей команды разработчиков.
Счастливого DDD проектирования!
PS: я написал этот пост менее чем за 10 минут, все листинги написаны с нуля в scratch, поэтому, пожалуйста: Если вы обнаружите какую-либо ошибку, опечатку и т.д, напишите, мне пожалуйста :)
Перевод статьи: Domain-Driven Design - The Factory in PHP