Decorator design pattern: minimal and practical example

E Ciotti
E Ciotti
Jan 17 · 3 min read

The decorator design pattern allows to add behaviour to an object without changing it, by implementing the behaviour in a new object that “wraps” the original one. Clear advantages are keeping the logic in separate classes, being able to compose them and being able to unit-test them in details.

Let’s take for example a basic URL download functionality. I just want to download an URL from a server, but I also want to cache the result, and also have a loop to try again if the network is unreliable.

Placing all this logic into a single class looks like the quickest thing to do, but it’s not recommended, you’ll have a big class doing too much, difficult to change. I’ll show you how to develop it in “layers”. I’ll show it in PHP7, but it’ll basically be the same concept in any other OOP language like Java, Python, Typescript.

Implementation

Define your functionality in the interface, and provide an implementation (e.g. I declare the functionality in an interface, then I implement it using a known HTTP lib like guzzle)

interface DownloaderInterface
{
public function download(string $url): string;
}


class GuzzleDownloader implements DownloaderInterface
{
/**
*
@var \GuzzleHttp\Client
*/
private $client;
public function __construct(\GuzzleHttp\Client $client)
{
$this->client = $client;
}

/**
* {
@inheritdoc}
*/
public function download(string $url): string
{
$response = $this->client->request('GET', $url);
return $response->getBody()->getContents();
}
}

You can now add the memory caching functionality by wrapping a DownloaderInterface implementation and add the caching behaviour. The download method will call the download on the “wrapped” internal downloader, stored as a class field.

class DownloaderCached implements DownloaderInterface
{
/**
*
@var DownloaderInterface
*/
private $downloader;

/**
*
@var string[]
*/
private static $urlToContentArrayCache = [];

/**
* DownloaderTryAgain constructor.
*
*
@param DownloaderInterface $downloader
*/
public function __construct(DownloaderInterface $downloader)
{
$this->downloader = $downloader;
}

/**
* {
@inheritdoc}
*/
public function download(string $url): string
{
if (isset(self::$urlToContentArrayCache[$url])) {
return self::$urlToContentArrayCache[$url];
}

$ret = $this->downloader->download($url);

self::$urlToContentArrayCache[$url] = $ret;

return $ret;
}
}

To use it, we just have to inject the native downloaded into the cache one. Simple.

$client = new \GuzzleHttp\Client();
$guzzleDownloader = new GuzzleDownloader($client);
$downloaderCached = new DownloaderCached($guzzleDownloader);
$downloaderCached->download("<your url>");

Hope you can see the advantages of this approach. Caching functionality is decoupled and can be easily independently changed, attached or not to the downloading functionality, without changing any class implementation. You’ll see more advantages when things gets more complicated in the next step.

Decorating further

I know want to have a functionality to re-attempt the download in case of temporary network failures. I’m then creating another a decorator wrapping any downloading functionality(I keep the code minimal to make it easer to read it)

class DownloaderTryAgain implements DownloaderInterface
{
/**
*
@var DownloaderInterface
*/
private $downloader;

/**
* DownloaderTryAgain constructor.
*
*
@param DownloaderInterface $downloader
*/
public function __construct(
DownloaderInterface $downloader
) {
$this->downloader = $downloader;
}


/**
* {
@inheritdoc}
*/
public function download(string $url): string
{
// (hardcoding the two params to keep the example simple)
return $this->downloadTryAgain($url, 5, 3);
}

/**
*
@param $url
*
@param $attemptsLeft
*
@param $sleepSeconds
*
*
@return mixed|string
*/
private function downloadTryAgain($url, $attemptsLeft, $sleepSeconds)
{
try {
return $this->downloader->download($url);
} catch (ServerUnavailableException $e) {
if ($attemptsLeft <= 0) {
throw new MaxAttemptsReached('Max downloads attempts reached', $e->getCode(), $e);
}

// sleep and try again for temp HTTP failures, linearly increasing sleep time
$sleepSecondsRounded = (int)round($sleepSeconds);
sleep(2);

return $this->downloadTryAgain($url, $attemptsLeft - 1, $sleepSeconds * 1.5);
}
}
}

Following the same usage approach below, I can now decorate the native downloader and obtain a downloader that tries again with guzzle library.

$downloaderTryAgain = new DownloaderTryAgain($guzzleDownloader);
$downloaderTryAgain->download('<unreliable server URL>');

Or I can combine all of them together to have a downloader that re-tries the downloads in case of failures, and caches the valid ones.

$tryAgainDownloader = new DownloaderTryAgain($downloaderCached);
$downloaderTryAgain->download('slow and unreliable url>');

A full working example with Unit-test is at this link

Thanks for reading, clap as much as you want if you find it useful

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade