
Domain-driven design советует создавать агрегаты и другие сложные объекты в фабриках. Но существует ли возможность запретить создание таких объектов вне фабрик? В PHP мы можем объявить конструктор класса приватным либо защищенным, и тогда объект этого класса можно создать только внутри статического метода этого же класса. Но это нарушает принцип единственной ответственности (SRP). Может есть другой способ?
Если мы хотим создавать объекты класса вне этого класса, его конструктор должен быть публичным. Значит ли это, что мы должны доверять другим разработчикам, что они не создадут объект вне его фабрики? Не обязательно.
Конструктор может контролировать откуда он вызван. Поможет в этом функция debug_backtrace.
Вас это смущает?
Да, название функции говорит о том, что она создана для отладки. Ну и что их этого? Для отладки лучше использовать отладчики, такие как Xdebug. Тут, хотя бы, ее использование оправдано.
<?php trait FactoryChecking { protected function checkFactory(string $factoryClass): void { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); foreach($trace as $traceItem) { if ($traceItem['class'] == $factoryClass) { return; } } throw new Exception('Cannot create class ' . static::class . ' outside of factory'); } } class ClassA { use FactoryChecking; public function __construct() { $this->checkFactory(Factory::class); } } class Factory { public function create(): ClassA { return new ClassA(); } }
Метод checkFactory понадобится в каждом классе, экземпляры которого должны создаваться в фабрике, но это не повод для использования наследования. Агрегация здесь мне тоже кажется не уместной. Так что, наверное, это подходящий случай для использования трейтов.
Это работает, но класс classA все еще имеет лишнюю ответственность — контролировать где создаются его экземпляры. Так что мы возвращаемся к частным/защищенным конструкторам. Существует ли метод создания экземпляра класса с частным/защищенным конструктором вне самого класса? С этим нам поможет модуль Reflection, который является частью ядра PHP.
<?php class ClassA { public const FRIEND_CLASSES = [Factory::class]; protected function __construct() {} } trait Constructor { protected function createObject(string $className, array $args = []) { if (!in_array(static::class, $className::FRIEND_CLASSES)) { throw new \Exception("Call to private or protected {$className}::__construct() from invalid context"); } $reflection = new ReflectionClass($className); $constructor = $reflection->getConstructor(); $constructor->setAccessible(true); $object = $reflection->newInstanceWithoutConstructor(); $constructor->invokeArgs($object, $args); return $object; } } class Factory { use Constructor; public function createA(): ClassA { return $this->createObject(ClassA::class); } }
Константа FRIEND_CLASSES в ClassA используется чтобы определить список классов в которых экземпляр класса ClassA может быть создан.
Теперь у класса ClassA нет лишней ответственности и его экземпляры могут быть созданы только в дружественных классах. Но что с производительностью? Что ж, проверим.
В качестве ориентира используем фабрику без проверок:
test.php <?php class ClassA { public function __construct() {} } class Factory { public function create(): ClassA { return new ClassA(); } } $start = microtime(true); $factory = new Factory(); for ($i = 0; $i < 100000; $i++) { $object = $factory->create(); } echo microtime(true) - $start;
> php test.php 0.12572288513184
Делаем тоже самое с debug_backtrace:
> php backtrace.php 0.26334500312805
И с Reflection:
> php reflect.php 0.52497291564941
Проверки, конечно же, занимают некоторое время. Использование debug_backtrace замедляет создание объекта в 2 раза, а Reflection в 4. Но важно понимать, что конструктор класса и фабрика ничего не делают. Мы замеряем время создания пустого объекта. В реальной ситуации разница будет меньше.
Что вы думает о самой идее? Используете ли вы какие-то методы чтобы избежать создания объектов вне фабрики?
P.S. Выше представлен перевод моей собственной статьи опубликованной чуть ранее на Medium в публике «Level Up Coding»: ссылка на статью.
ссылка на оригинал статьи https://habr.com/ru/post/659205/
Добавить комментарий