Статьи

Легкий дизайн шаблона и неизменность: идеальное соответствие

Шаблон flyweight — это относительно неизвестный шаблон проектирования в PHP. Фундаментальный принцип, лежащий в основе шаблона flyweight, заключается в том, что память может быть сохранена путем запоминания объектов после их создания. Затем, если те же объекты необходимо использовать снова, ресурсы не нужно тратить впустую, воссоздавая их.

Balloon lifting a barbell

Вы можете думать о шаблоне flyweight как о модификации обычной фабрики объектов. Модификация заключается в том, что вместо того, чтобы постоянно создавать новые объекты, фабрика должна проверять, уже создал ли он запрошенный объект. Если это так, он должен вернуть этот экземпляр, а не создавать объект снова.

Хорошим примером использования шаблона flyweight будет приложение, которое должно загружать большие файлы. Эти файлы были бы нашими мухи.

Мухи

Одна важная особенность мухи — это то, что они неизменны . Это означает, что они не могут быть изменены после их создания. Это потому, что наша фабрика может гарантировать, что она запомнила правильный объект, если она также может гарантировать, что объект, который она изначально создала, не был изменен.

Ниже приведен очень простой пример летучего объекта для файла. Мы можем сказать, что оно является неизменным, потому что свойство «data» не может быть изменено после вызова конструктора. Нет метода setData.

class File
{
    private $data;

    public function __construct($filePath)
    {
        // Check to make sure the file exists
        if (!file_exists($filePath)) {
            throw new InvalidArgumentException('File does not exist: '.$filePath);
        }

        $this->data = file_get_contents($filePath);
    }

    public function getData()
    {
        return $this->data;
    }
}

Фабрика Полусухого веса

Наша фабрика по производству легких грузов должна иметь возможность получать уникальный идентификатор создаваемых ею объектов. Затем он может использовать этот уникальный идентификатор, чтобы проверить, создал ли он уже тот объект, для которого он запрашивается. Во многих случаях параметры нашего метода фабрики будут формировать соответствующий уникальный идентификатор для нашего веса; как и должно быть, если мы вызовем нашу фабрику с теми же параметрами, она даст тот же результат.

В приведенном ниже примере мы используем свойство «files» в качестве ассоциативного массива для хранения созданных объектов. Наиболее очевидным выбором для уникального идентификатора является путь к файлу, так что это будет ключ к элементам в массиве.

 class FileFactory
{
    private $files = array();

    public function getFile($filePath)
    {
        // If the file path isn't in our array, we need to create it
        if (!isset($this->files[$filePath])) {
            $this->files[$filePath] = new File($filePath);
        }

        return $this->files[$filePath];
    }
}

Теперь мы можем использовать класс FileFactory для получения файлов, не беспокоясь о загрузке их несколько раз!

 $factory = new FileFactory;

$myLargeImageA = $factory->getFile('/path/to/my/large/image.png');
$myLargeImageB = $factory->getFile('/path/to/my/large/image.png');

if ($myLargeImageA === $myLargeImageB) {
    echo 'Yay, these are the same object!'.PHP_EOL;
} else {
    echo 'Something went wrong :('.PHP_EOL;
}

Замечание о потоках

Шаблон flyweight особенно полезен в многопоточной среде. Неизменность неизменяемых объектов гарантирует, что их можно безопасно использовать одновременно несколькими потоками. Это потому, что нет никаких шансов, что два потока попытаются изменить один и тот же объект одновременно.

Если вы используете шаблон flyweight в многопоточной среде, вы должны рассмотреть фабричный метод как критический раздел. Блокировки или аналогичные элементы управления должны использоваться для обеспечения того, чтобы два потока не пытались создать объект одновременно.

Мышление PHP

Ранее я упоминал, что шаблон flyweight — это относительно неизвестный шаблон проектирования в PHP. Одна из основных причин этого заключается в том, что использование памяти не учитывается многими разработчиками PHP. Большинству наших PHP-приложений не нужно много работать, и их экземпляры живут всего несколько миллисекунд, пока обрабатывают входящий HTTP-запрос.

Однако, если мы используем шаблон flyweight в долго работающем приложении, мы должны знать об утечках памяти. В PHP память будет выделяться объекту до тех пор, пока ссылки на него существуют в рамках приложения. Поскольку фабрика с наименьшим весом сохраняет ссылки на каждый создаваемый объект, когда число объектов, которые можно было бы попросить создать, бесконечно, приложению в конечном итоге не хватит памяти.

Шаблон flyweight лучше всего подходит для сценариев, в которых число объектов, которые необходимо будет создать и запомнить фабрике, является конечным и совместимым с ограничениями памяти в приложении. Это означает, что приложением нельзя манипулировать экстенсивным созданием и запоминанием объектов, пока оно не выйдет из строя.

Перечень с Flyweights

Оптимизация памяти, однако, не единственная причина, по которой вы решили бы использовать шаблон flyweight. Шаблон flyweight также может быть полезен для создания объектов перечисления. Doctrine DBAL является примером библиотеки, которая делает это. Это помогает разработчикам писать независимый от платформы код, который будет беспрепятственно работать со многими различными уровнями хранения.

В этой библиотеке объекты «Тип» используются для преобразования между значениями базы данных и значениями PHP. Существуют различные объекты «Тип» для строк, целых чисел, логических значений, чисел с плавающей точкой, массивов, дат и многого другого.

Вот очень упрощенная версия абстрактного класса Type из Doctrine DBAL:

 abstract class Type
{
    const INTEGER  = 'integer';
    const STRING   = 'string';
    const DATETIME = 'datetime';

    private static $_typeObjects = array();

    private static $_typesMap = array(
        self::INTEGER  => 'Doctrine\DBAL\Types\IntegerType',
        self::STRING   => 'Doctrine\DBAL\Types\StringType',
        self::DATETIME => 'Doctrine\DBAL\Types\DateTimeType',
    );

    public static function getType($name)
    {
        if (!isset(self::$_typeObjects[$name])) {
            if (!isset(self::$_typesMap[$name])) {
                throw DBALException::unknownColumnType($name);
            }

            self::$_typeObjects[$name] = new self::$_typesMap[$name]();
        }

        return self::$_typeObjects[$name];
    }

    // ...
}

Как видите, шаблон flyweight используется для обеспечения создания только одного объекта для каждого типа. Если разработчикам необходимо получить объект Type, они могут статически вызывать метод getType, например:

 $integerType = Type::getType(Type::INTEGER);

Использование шаблона flyweight таким способом помогает уменьшить объем памяти библиотеки, но также помогает и другими способами.

Объекты перечисления имеют больше смысла, когда используется шаблон flyweight. Без этого происходят странные вещи. Возьмите это к примеру:

 $type1 = Type::getType(Type::INTEGER);
$type2 = Type::getType(Type::INTEGER);

if ($type1 === $type2) {
    echo 'Yay, you used the flyweight pattern!'.PHP_EOL;
} else {
    echo 'Well this is confusing :('.PHP_EOL;
}

Без теста с наименьшим весом вышеупомянутый тест не пройдет. Это может быть особенно запутанным для новых, и даже опытных разработчиков, в течение долгого дня!

Другим примером перечислимых объектов, использующих шаблон flyweight, является php-enum от Marc Bennewitz.

Резюме

Шаблон flyweight наиболее полезен в приложениях, где совместное использование объектов может значительно сократить использование памяти. Этот шаблон, конечно, не тот, с которым мы обычно сталкиваемся в приложениях PHP, однако существуют сценарии, в которых он может быть полезен. Хотя шаблон предназначен для сокращения использования памяти, он используется неправильно, могут возникнуть утечки памяти. Объекты перечисления, как правило, имеют больше смысла, когда используется шаблон flyweight, поскольку существует только один экземпляр объекта каждого значения.