Запрет создания объекта вне фабрики в PHP

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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/


Интересные статьи

Интересные статьи

Наукоемкое производство в наши дни нельзя представить без применения самых новейших методов управления и бизнес-инжиниринга.  Если еще каких-то лет 15 назад наукоемкое производство в России почти...
Мы живем в век, когда каждая задача, которую мы выполняли на нашей локальной машине, теперь выполняется в облаке. Гибкость, надежность, совместная работа, более низкие затраты на оборудование и програ...
Каждый лишний элемент на сайте — это кнопка «Не купить», каждая непонятность или трудность, с которой сталкивается клиент — это крестик, закрывающий в браузере вкладку с вашим интернет-магазином.
История развития построения веб-приложений на языке программирования Java с примерами их использования на временном промежутке от появления спецификации сервлетов до сегодняшнего дня. ...
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...