The Decorator Pattern lets you to easily extend a given set of objects (either grouped via class inheritance or interface) and extend their functionality in a way that avoids needlessly duplicating code.
Our example today will hopefully be pretty close to form, and while generally contrived, will simply illustrate the use of the Decorator Pattern.
Let's say for instance, you run a video rental service. Let's call it... SmedRocks. Now your crazy programmers have already built your inventory tracking system as a web service, and you have no control over how they have implemented it, but you have to fight the good fight and soldier on. Features must be implemented. To make this even simpler, our API has one function:
getVideos - This returns all of the titles we have, no matter if they are currently rented or not.
Now getVideos returns a JSON array like so:
{ { 'title': 'Some Movie', 'status': 'rented' }, { 'title': 'Some Other Movie', 'status': 'available' } } |
I'm sure you can already see the problem with how it's returning data. Rented movies and available movies are all mixed in! What a pain!
Ok, so let's look at what we need to do to get started. Let's make our API. This isn't how you would ACTUALLY do this, it's just an example to get us moving along:
class MovieApi { public function getVideos() { return json_decode(file_get_contents('http://example.org/rest/getVideos.json')); } } |
Excellent! We can now get our videos, but wait -- we're in the middle of building this and example.org seems to have gone offline. This isn't going to help us get this going! So let's refactor real quick so we can keep going on other things.
interface MovieApiInterface { public function getVideos(); } class LiveMovieApi implements MovieApiInterface { public function getVideos() { return json_decode(file_get_contents('http://example.org/rest/getVideos.json')); } } class OfflineMovieApi implements MovieApiInterface { public function getVideos() { $json = '{{"title": "Movie 1", "status": "rented"},{"title": "Movie 2", "status": "available"}}'; return json_decode($json); } } |
Look at that! We've now defined a central MovieApiInterface which both implementations conform to, and have both an online and offline implementation which will also ultimately make writing tests easier.
Ok, now for the juicy part: we need to be able to ask this system for just rented or available movies. We could extend each API implementation, but that's going to be some duplication we can live without.
Enter the Decorator Pattern.
With the decorator pattern, we can build an object that accepts a MovieApiInterface object in it's constructor, and provides a uniform higher-level way to interact with our lower-level API.
Again, this is basic code to get you thinking, not optimized production-ready code.
class SpecificMovieFinder implements MovieApiInterface { protected $movie_api; public function __construct(MovieApiInterface $movie_api) { $this->movie_api = $movie_api; } public function getVideos() { return $this->movie_api->getVideos(); } public function getRentedMovies() { $movies = $this->getVideos(); $rented_movies = array(); foreach ($movies as $movie) { if ($movie->status == 'rented') { $rented_movies[] = $movie; } } return $rented_movies; } public function getAvailableMovies() { $movies = $this->getVideos(); $available_movies = array(); foreach ($movies as $movie) { if ($movie->status == 'available') { $available_movies[] = $movie; } } return $available_movies; } } |
Now we have a simpler API that actually conforms to how we will use it in practice instead of how the API designers built the service. This is great! They get their way, and we get ours. Everyone wins.
So how do we use it?
// development configuration $offline_api = new SpecificMovieFinder(new OfflineMovieApi()); // live configuration $online_api = new SpecificMovieFinder(new LiveMovieApi()); |
Now we get the same functionality on multiple implementations, and as an added bonus, our SpecificMovieFinder also still implements the MovieApiInterface allowing us to use it interchangeably with any other service that may need our api down the line!
My good friend Beau Simensen pointed out that unfortunately, my example is a little deficient. I'll let his gist do the talking:
Thanks Beau!
What are some other places you can think of using this pattern?
designbymobius
December 21, 2012 @ 9:17 am
I really like the offline / online implementation. I’m always developing with my feet too close to the
fireproduction server and this would be a super-handy way to work a more comfortable distance away while ensuring my implementation is going to hold up when it comes to the real deal.Thanks for sharing!
Matt Frost
December 21, 2012 @ 9:26 am
Where are your tests? 😛 Good stuff dude!
cordoval
December 21, 2012 @ 2:29 pm
where are the tests and GH repo?
Jacob Mather
December 21, 2012 @ 2:34 pm
It appears I seem to have set some standards… 😀