Thursday, February 04, 2010

Practical Php Patterns: Proxy

This post is part of the Practical Php Pattern series.
 
The structural pattern of today is the Proxy pattern, an enhancement on the simple handler (or pointer) used as a reference to an object: this pointer is substituted by a Proxy object which interposes itself between the Client and the execution of actual work, adding an hook that can be exploited for many different goals.
Technically, this pattern interposes a Proxy object between Client and a RealSubject, maintaining the Subject interface and delegating its methods in different ways. A Proxy can really do anything trasparently: lazy creation of the RealSubject or loading of data, exchange of messages with other machines, copy-on-write strategies.
The analogy that comes to mind is an http proxy, which clients such as browsers and applications rely on to contact http servers. The proxy can accomplish useful tasks while managing the connections, like access control and caching of large downloaded files.
While Proxy objects make room for optimizations, the abstraction they maintain as implementing the same interface can often result in performance issues (it becomes a leaky abstraction), as in the case of Orms Proxy objects that is treated later in this post.

The object graph of a Proxy pattern is similar to the Decorator's one in structure, but the intent conveyed is different. A Decorator adds behavior dynamically to objects, while a Proxy controls the access from the Clients. Moreover, a Proxy may lazy create the RealSubject only when it's needed or work with it via other means but it does not usually compose it.
Participants:
  • Client: depends on a Subject implementation.
  • Subject: abstraction of the RealSubject.
  • RealSubject: accomplish expensive work or contains a lot of data.
  • Proxy: provides to Client a reference that conforms to Subject, while creating or communicating with the RealSubject instance expensively only when needed.
 These are two examples of the wide usage of the Proxy pattern:
  • Object-relational mappers create Proxies on-the-fly as subclasses of Entity classes, to accomplish lazy loading (virtual proxy). This proxies override all the Entity methods prepending a loading procedure before delegating the action, and do not contain data before a method is actually called. Orms proxies support bidirectional relationships between objects without loading the entire database, since they are put at the border of the object graph section currently loaded.
  • Java RMI uses Proxy objects for remoting (remoting proxy). The proxies serialize parameters when their methods are called and perform a request over the network to delegate the call to the real object on another node. This technique allows working with remote objects transparently, without noticing they are not on the same machine, but because of this transparency it is prone to slow down the execution.
The code sample implements an ImageProxy that postpones the loading of an image's data.
<?php
/**
 * Subject interface.
 * Client depends only on this abstraction.
 */
interface Image
{
    public function getWidth();

    public function getHeight();

    public function getPath();

    /**
     * @return string   the image's byte stream
     */
    public function dump();
}

/**
 * Abstract class to avoid repetition of boilerplate code in the Proxy
 * and in the Subject. Only the methods which can be provided without
 * instancing the RealSubject are present here.
 */
abstract class AbstractImage implements Image
{
    protected $_width;
    protected $_height;
    protected $_path;
    protected $_data;

    public function getWidth()
    {
        return $this->_width;
    }

    public function getHeight()
    {
        return $this->_height;
    }

    public function getPath()
    {
        return $this->_path;
    }
}

/**
 * The RealSubject. Always loads the image, even if no dump of the data
 * is required.
 */
class RawImage extends AbstractImage
{
    public function __construct($path)
    {
        $this->_path = $path;
        list ($this->_width, $this->_height) = getimagesize($path);
        $this->_data = file_get_contents($path);
    }

    public function dump()
    {
        return $this->_data;
    }
}

/**
 * Proxy. Defers loading the image data until it becomes really mandatory.
 * This class does its best to postpone the very expensive operations
 * such as the actual loading of the BLOB.
 */
class ImageProxy extends AbstractImage
{
    public function __construct($path)
    {
        $this->_path = $path;
        list ($this->_width, $this->_height) = getimagesize($path);
    }

    /**
     * Creates a RawImage and exploits its functionalities.
     */
    protected function _lazyLoad()
    {
        if ($this->_realImage === null) {
            $this->_realImage = new RawImage($this->_path);
        }
    }

    public function dump()
    {
        $this->_lazyLoad();
        return $this->_realImage->dump();
    }
}

/**
 * Client class that does not use the data dump of the image.
 * Passing blindly a Proxy to this class and to other Clients makes sense
 * as the data would be loaded anyway when Image::dump() is called.
 */
class Client
{
    public function tag(Image $img)
    {
        return '<img src="' . $img->getPath() . '" alt="" width="'
             . $img->getWidth() . '" height="' 
             . $img->getHeight() . '" />';
    }
}

$path = '/home/giorgio/shared/Immagini/kiki.png';
$client = new Client();

$image = new RawImage($path); // loading of the BLOB takes place
echo $client->tag($image), "\n";

$proxy = new ImageProxy($path);
echo $client->tag($proxy), "\n"; // loading does not take place even here

5 comments:

Sean said...

Seems that your code in the last example is a bit too long.

Giorgio said...

Thank you for your feedback, I have truncated every line that was more than 80 characters long. :)

Anonymous said...

nice post. thanks.

Morten Engelsmann said...

Thanks for your posts. Is the first call with new RawImage($path) necessary for the code to work? For the image to display?

Giorgio said...

Morten,
no it's not necessary. It's showing the difference between the original and the proxy (you use them in the same way).

ShareThis