Once again my Saturday evening is set in the local pub (well: the one in Galway I go to when I'm here anyhow. I go to this place more than I do my "local" at home), with a Guinness and the laptop in front of me. I've not actually done this for a while, coming to think of it.
Right, so today's random code exercise is brought to me via Stack Overflow, with some bod asking a question about how to decouple knowledge of the implementation of a task from the code that needs to execute the task. They ask it in a slightly different way, but that's the gist of it. Go read it anyhow.
This very thing has come up at work a bit recently... we have a tendency to have a single class which has various different implementations of the same task, but for different situational variations. We could all see this was a bit of a smell, but weren't initially sure how we should approach it. Enter my mate Brian and his ability to remember things that he's read (damn him), and at some stage he's read that - apparently turgid to the point of being close to unreadable - book "Design Patterns [etc]" which at least identifies a bunch of design patterns we should all be aware of when solving problems. I think I'll stick with the copy of "Head First Design Patterns" sitting on my desk. It's eminently readable.
Right, so this sounds like a case for the Strategy Pattern, which - according to Wikipedia - "… is a software design pattern that enables an algorithm's behavior to be selected at runtime". Basically you have a class which has a method to perform a task, but the implementation of the task is handed off to a collaborator which is implemented separately.
By way of example I'm gonna use a very contrived situation to demonstrate the technique. NB: this is different from my answer on Stack Overflow, but amounts to the same thing. Here we the notion of a Name, and a NameDecorator; and the requirement is to render the name in two ways: plain text as "[lastName], [firstName]" and with a mark-up template to effect something like "[firstName] [lastName]". A naïve implementation might be as follows:
First we just have a very generic Name class:
class Name {
public $firstName;
public $lastName;
function __construct($firstName, $lastName){
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
That's not really part of the problem here, but the code revolves around this. And now our Decorator class:
class NameDecorator {
private $name;
private $fullNameTemplate;
function __construct($name, $fullNameTemplate){
$this->name = $name;
$this->fullNameTemplate = $fullNameTemplate;
}
function renderFullNameAsPlainText(){
return sprintf('%s, %s', $this->name->lastName, $this->name->firstName);
}
function renderFullNameWithHtmlTemplate(){
return sprintf($this->fullNameTemplate, $this->name->firstName, $this->name->lastName);
}
}
And sample usage:
$name = new Name('Zachary', 'Cameron Lynch');
$template = '%s <strong>%s</strong>';
$nameDecorator = new NameDecorator($name, $template);
$rendered = $nameDecorator->renderFullNameAsPlainText();
echo "Simple rendering: $rendered<br>";
$rendered = $nameDecorator->renderFullNameWithHtmlTemplate();
echo "Rendering with template: $rendered<br>";
And result:
Simple rendering: Cameron Lynch, Zachary
Rendering with template: Zachary Cameron Lynch
Here's the problem. Sometimes we need to render the name using a template: sometimes we don't. Some entire usages of this decorator will never need the template, but we still need to initialise the decorator with it.
This could be easily solved by passing the template with the rendering call, instead of init-ing the whole object with it:
function renderFullNameWithHtmlTemplate($template){
return sprintf($template, $this->name->firstName, $this->name->lastName);
}
And if that was the full requirement, that'd be fine. But the requirement is such that we need to call this code from other code which doesn't want to fuss about with templates and which method to call, it just wants to call a render method and dependent on how the decorator has been configured, will use whatever rendering implementation the decorator has decided upon.
We could solve this with an abstract notion of what the NameDecorator is, and have concrete implementations for each variation:
abstract class NameDecorator {
protected $name;
function __construct($name){
$this->name = $name;
}
abstract public function render();
}
class PlainTextNameDecorator extends NameDecorator {
function render(){
return sprintf('%s, %s', $this->name->lastName, $this->name->firstName);
}
}
class TemplatedNameDecorator extends NameDecorator {
protected $fullNameTemplate;
function __construct($name, $fullNameTemplate){
parent::__construct($name);
$this->fullNameTemplate = $fullNameTemplate;
}
function render(){
return sprintf($this->fullNameTemplate, $this->name->firstName, $this->name->lastName);
}
}
Now we just use separate Decorator classes: PlainTextNameDecorator and TemplatedNameDecorator, but we have the one unified
render()
method:
$name = new Name('Zachary', 'Cameron Lynch');
$nameDecorator = new PlainTextNameDecorator($name);
$rendered = $nameDecorator->render();
echo "Simple rendering: $rendered<br>";
$template = '%s <strong>%s</strong>';
$nameDecorator = new TemplatedNameDecorator($name, $template);
$rendered = $nameDecorator->render();
echo "Rendering with template: $rendered<br>";
TBH, if the full requirement was as I've stated here, that'd probably do. But... same as with my discussion "Decorator Pattern vs simple inheritance", it's not very scalable. This works for one variation, but the number of inherited classes gets out of hand very quickly with each method that potentially needs its implementation abstracted. For example if one had a Person class and two methods which need handling:
renderName()
and renderAddress()
... to cover the bases with "plain text" and "templated" options, one would need classes like this:- PlainNamePlainAddressPersonDecorator
- PlainNameTemplatedAddressPersonDecorator
- TemplatedNamePlainAddressPersonDecorator
- TemplatedNameTemplatedAddressPersonDecorator
And if we wanted to include a phone number? Eight classes. Basically it's m^n classes, where m is the ways to decorate things, and n is the number of items needing decoration. So if we also had a need to provide for JSON decoration for these three: up from eight to 27 possible variations. Yikes. Not. Scalable.
A more scalable approach is to use composition over inheritance (the more I program, the more I find this to be the case), and use the Strategy Pattern. This basically means you extract just the method functionality into a collaborator, and just tell your main class about the collaborator.
class NameDecorator {
private $renderingHandler;
private $name;
function __construct($name, $renderingHandler){
$this->name = $name;
$this->renderingHandler = $renderingHandler;
}
function render(){
return $this->renderingHandler->render($this->name->firstName, $this->name->lastName);
}
}
Here NameDecorator knows it can render stuff, but it itself has no idea or care about the detail of how to render stuff. All it is doing is exposing a uniform API to the calling code: it can simply call
render()
:
$rendered = $nameDecorator->render();
echo "Simple rendering: $rendered<br>";
Before doing that though, we need to initialise the decorator with a handler:
$nameDecorator = new NameDecorator($name, new DefaultRenderingHandler());
And it's the handler that knows its own various pre-requisites:
class DefaultRenderingHandler {
function render($firstName, $lastName){
return "$lastName, $firstName";
}
}
class TemplatedRenderingHandler {
private $template;
function __construct($template){
$this->template = $template;
}
function render($firstName, $lastName){
return sprintf($this->template, $firstName, $lastName);
}
}
These classes just focus on what it is to render a name. The default one just gets on with it, whereas the templated one needs to be initialised with a template. But these sort of conceits are limited to the element that needs them: the Decorator itself doesn't need to know the templated handler needs a template. All it needs is a collaborator which implements a
render(firstName, lastName)
method.If we have further rendering requirements (say an address, as per earlier), we just need the collaboration for that. So the scaling for this model is m*n, not m^n. Obviously better. Of course one might not need every variation, but it's more the rapidity of things getting out of hand which is the important point here.
It also keeps things more focused: each collaborator simply needs to focus on what it needs to deal with, and not anything else. And the decorator itself just worries about providing its API.
It might seem like a lot of work to create a whole class for a single method, but - really - the class boilerplating isn't much is it? One could use stand-alone functions for this:
class NameDecorator {
private $renderingHandler;
private $name;
function __construct($name, $renderingHandler){
$this->name = $name;
$this->renderingHandler = $renderingHandler;
}
function render(){
return ($this->renderingHandler)($this->name->firstName, $this->name->lastName);
}
}
$renderAsPlainText = function ($firstName, $lastName) {
return "$lastName, $firstName";
};
$template = '%s <strong>%s</strong>';
$renderWithTemplate = function ($firstName, $lastName) use ($template) {
return sprintf($template, $firstName, $lastName);
};
$name = new Name('Zachary', 'Cameron Lynch');
$nameDecorator = new NameDecorator($name, $renderAsPlainText);
$rendered = $nameDecorator->render();
echo "Simple rendering: $rendered<br>";
$nameDecorator = new NameDecorator($name, $renderWithTemplate);
$rendered = $nameDecorator->render();
echo "Rendering with template: $rendered<br>";
See how I'm just using functions instead of whole classes here? But that's pretty poor code organisation IMO, so just stick 'em in classes.
The original question on Stack Overflow has asked for a variation and I think the Factory Method Pattern might be the way to go there. But... I'll think about that over night and perhaps come up with something tomorrow. I've got a bunch of downtime (this is for you, Andy Myers) at the airport waiting for a flight tomorrow afternoon.
Righto.
--
Adam
PS: four pints, that one.