Flyweight pattern using Mixins

Refactoring

In my previous article among other patters I was telling about Flyweight. That is about the objects designed so to minimize memory use by sharing as much data as possible with other similar objects. You can find plenty of Flyweight implementation examples in Internet, though it will be mostly variations of Multiton, a collection (map) keeping only instance per every identical object. And I decided to follow that unwritten rule as it seems to be the simplest way to show the idea. In some cases it is much more efficient to share not the instance itself, but weighty parts among the instances. Imagine, you have user object containing properties and methods relevant for any user. However registered user has extended set of properties and methods. Moderator or admin contain even more. First of all you may think of user as a base class extended by registered user, moderator, admin. That is an OCP friendly solution, but not a Flyweight. Let’s keep the additional functionality in separate classes, but mix it into user object when necessary. For Mixin pattern PHP 5.4 provides a gorgeous tool – traits. However as I see it, the object value inherited from the traits is getting a part on the target object. Thus, in the collection it won’t be shared data from the memory use prospective. But we want to keep in the collection only almost bare user objects and the references to the shared data, don’t we?

Flyweight using Mixins

Well, let’s have an abstract class Mixinable, which gives to any successor ability to lazy retrieve the requested functionality by the references kept in an array for each concrete successor (user) object instance. So, we create a user object passing into the constructor set of mixins (e.g. UserRegistered, UserModerator) and push it into the collection. The collection comprises now user object with references on the given mixins, but not the data of the mixins. When we extract the user object instance from the collection to consume, we can request mixin-related data (e.g. isModerator). At that moment PHP makes its magic and we get expected ‘true’ from shared mixin.

<?php
/*
 * @category Design Pattern Tutorial
 * @package Flyweight Sample
 * @author Dmitry Sheiko <[email protected]>
 * @link http://dsheiko.com
 */
/**
 * Shared properties and methods
 */
class Mixin_UserAdmin
{
    public $isAdmin = true;
    public $description = 'Admin user has a lot of rights';
}
class Mixin_UserModerator
{
    public $isModerator = true;
    public $description = 'Moderator can do something';
}
class Mixin_UserRegistered
{
    public $description = 'Registered user can login...';
    public $isRegistered = true;
}
/**
 * Flyweight object
 * Keeps inside only data which can't be shared, and references on shared data *
 */
abstract class Mixinable_Abstract
{
    protected $_mixins;

    // Mix in properties and methods from the list of provided objects
    public function  __construct(array %26$mixins)
    {
        $this->_mixins = $mixins;
    }
    public function  __get($name)
    {
        if (property_exists($this, $name)) {
            return $this->$name;
        } elseif ($this->_mixins) {
            foreach ($this->_mixins as $refObj) {
                if (property_exists($refObj, $name)) {
                    return $refObj->$name;
                }
            }
        }
        return null;
    }
    public function __call($name, $args)
    {
        if (method_exists($this, $name)) {
            return call_user_func_array(array($this, $name), $args);
        } elseif ($this->_mixins) {
            foreach ($this->_mixins as $refObj) {
                if (method_exists($refObj, $name)) {
                    call_user_func_array(array($refObj, $name), $args);
                }
            }
        }
        return null;
    }
}
/**
 * Concrete flyweight
 */
class User extends Mixinable_Abstract
{
    public $name;
}
/**
 * FlyweightFactory object
 */
class UserFactory
{
    private static function _getMixins($role)
    {
        static $_mixinMap;
        if ($_mixinMap === null) {
            $_mixinMap = array(
                "registered" => array(new Mixin_UserRegistered()),
                "moderator" => array(new Mixin_UserModerator(), new Mixin_UserRegistered()),
                "admin" => array(new Mixin_UserAdmin(), new Mixin_UserRegistered()),
            );
        }
        if (!isset ($_mixinMap[$role])) {
            throw new Exception("Unrecognized role");
        }
        return $_mixinMap[$role];
    }
    public static function getInstance($name, $role)
    {
        $user = new User(self::_getMixins($role));
        $user->name = $name;
        return $user;
    }
}
/**
 * Usage
 */
$user1 = UserFactory::getInstance('Jon Snow', 'registered');
printf ("User name: %s, description: %s\n", $user1->name, $user1->description);
$user2 = UserFactory::getInstance('Tywin Lannister', 'admin');
printf ("User name: %s, description: %s\n", $user2->name, $user2->description);

// Output:
// User name: Jon Snow, description: Registered user can login...
// User name: Tywin Lannister, description: Admin user has a lot of rights

The same can be achieved in JS. ES5 provides configurable getter, which can be used as __get() magic method of PHP.

/*
 * @category Design Pattern Tutorial
 * @package Flyweight Sample
 * @author Dmitry Sheiko <[email protected]>
 * @link http://dsheiko.com
 */
(function() {

var MixinUserAdmin = Object.create(Object.prototype, {
    description : {
        value : 'Admin user has a lot of rights',
        enumerable: true
    },
    isAdmin : {
        value : true,
        enumerable: true
    }
});
var MixinUserRegistered = Object.create(Object.prototype, {
    description : {
        value: 'Registered user can login...',
        enumerable: true
    }
});

var MixinableUser = Object.create(Object.prototype, {
    mixins: {
        value: [],
        enumerable: true,
        configurable: true,
        writable: true
    },
    description : {
        get: function() {
            for (var i in this.mixins) {
                if (this.mixins[i].hasOwnProperty('description')) {
                    return this.mixins[i].description;
                }
            }
            return null;
        }
    }
});

var User = function(){};
User.prototype = Object.create(MixinableUser, {
    name : {
        value : null,
        enumerable: true,
        configurable: true,
        writable: true
    }
});

var makeUser = function(name, role) {
    var mixinMap = {
        'admin' : [MixinUserAdmin],
        'registered' : [MixinUserRegistered]
    }
    var u = new User();
    if (typeof mixinMap[role] === 'undefined') {
        throw new Error('Insufficient role');
    }
    u.mixins = mixinMap[role];
    u.name = name;
    return u;
};

/**
 * Usage
 */

var user1 = makeUser('Jon Snow', 'registered');
var user2 = makeUser('Tywin Lannister', 'admin');
console.log('User name: ' + user1.name + ', description: ' + user1.description);
console.log('User name: ' + user2.name + ', description: ' + user2.description);

// Output:
// User name: Jon Snow, description: Registered user can login...
// User name: Tywin Lannister, description: Admin user has a lot of rights

}());