Flyweight and immutability design pattern: a perfect match

0


The flyweight pattern is a relatively unknown design pattern in PHP. The fundamental principle behind the flyweight model is that memory can be saved by remembering objects after they are created. Then, if the same objects are to be reused, resources should not be wasted recreating them.

You can think of the flyweight model as a modification of a conventional object factory. The change being that rather than just creating new objects all the time, the factory should check to see if it has already created the requested object. If so, it should return that instance rather than recreating the object.

A good use case for the flyweight model would be an application that needs to load large files. These files would be our flyweight objects.

The flyweight object

An important characteristic of flyweight objects is that they are unchanging. This means that they cannot be changed once they have been built. This is because our factory can only guarantee that it remembered the correct object if it can also guarantee that the object that it originally created has not been altered.

Below is an example of a very simple flyweight object for a file. We can say that it is immutable because the ‘data’ property cannot be changed after the constructor call. There is no ‘setData’ method.

class File
{
    private $data;

    public function __construct($filePath)
    {
        
        if (!file_exists($filePath)) {
            throw new InvalidArgumentException('File does not exist: '.$filePath);
        }

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

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

The flyweight factory

Our flyweight factory must be able to obtain a unique identifier for the flyweight objects it creates. He can then use this unique identifier to check if he has already created the object that is requested of him. In many scenarios, our factory method parameter (s) will form an appropriate unique identifier for our flyweight; as it should be if we call our factory with the same parameters, it will produce the same result.

In the example below, we are using the ‘files’ property as an associative array to store the objects that have been created. The most obvious choice for the unique identifier is the file path, so this will be the key to the array elements.

class FileFactory
{
    private $files = array();

    public function getFile($filePath)
    {
        
        if (!isset($this->files[$filePath])) {
            $this->files[$filePath] = new File($filePath);
        }

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

We can now use the FileFactory class to get files without worrying about loading them multiple times!

$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;
}

A note on threading

The flyweight model is particularly useful in a multithreaded environment. The immutability of flyweight objects ensures that they can be safely used by multiple threads at the same time. This is because there is no chance that two threads will try to change the same object simultaneously.

If you are using the flyweight model in a multithreaded environment, you should consider the factory method as a critical section. Locks or similar controls should be used to ensure that two threads do not try to create an object at the same time.

Think PHP

Earlier I mentioned that the flyweight model is a relatively unknown design model in PHP. One of the main reasons for this is that memory usage is not a consideration for many PHP developers. Most of our PHP applications don’t have to do a lot of work, and their instances only live a few milliseconds while they process the incoming HTTP request.

If, however, we are using the flyweight model in a long-lasting application, we should be aware of memory leaks. In PHP, memory will remain allocated to an object as long as references to it exist within the framework of the application. Because a flyweight factory keeps references to every object it creates, when the number of objects it could be asked to create is infinite, the application will eventually run out of memory.

The flyweight model is best suited to scenarios where the number of objects that the plant will have to create and store is finite and compatible with the memory constraints of the application. That is, the app could not be manipulated to create and remember objects extensively until it crashed.

Enumeration with flyweight

Optimizing memory, however, is not the only reason you would choose to implement the flyweight model. The flyweight pattern can also be useful for creating listing objects. The DBAL Doctrine is an example of a library that does this. It helps developers write platform independent code that will work seamlessly with many different storage layers.

In this library, ‘Type’ objects are used to convert between database values ​​and PHP values. There are different ‘Type’ objects for strings, integers, booleans, floats, arrays, dates and more.

Here is a very simplified version of the abstract class ‘Type’ of the DBAL Doctrine:

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

    private static $_typeObjects = array();

    private static $_typesMap = array(
        self::INTEGER  => 'DoctrineDBALTypesIntegerType',
        self::STRING   => 'DoctrineDBALTypesStringType',
        self::DATETIME => 'DoctrineDBALTypesDateTimeType',
    );

    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];
    }

    
}

As you can see, the flyweight model is used to require that only one object for each type be created. If developers need to get a ‘Type’ object, they can call the ‘getType’ method statically, like this:

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

Using the flyweight model in this way helps reduce the library’s memory footprint, but it also helps in other ways.

Enumeration objects make more sense when the flyweight model is used. Without it, strange things happen. Take this for example:

$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;
}

Without the flyweight model, the above test will not pass. This can be especially confusing for new and even experienced developers on a long day!

Another example of enumeration objects using the flyweight model is php-enum by Marc Bennewitz.

Summary

The flyweight model is more beneficial in applications where sharing objects can significantly reduce memory usage. This model is certainly not the one we will commonly encounter in PHP applications, but there are scenarios where it can be useful. Although the model is designed to reduce memory usage, when used incorrectly, memory leaks can occur. Enumeration objects tend to make more sense when the flyweight model is used, because there is never only one instance of an object of each value.


Share.

Leave A Reply