Tuesday, April 2, 2013

Introduction to the Decorator Pattern

One of my favorite software development patterns is the Decorator Pattern. After having taken on so-called brown-field projects, and discussing design patterns with others, I notice so many areas where this pattern could have saved a big ball of mud from forming. It happens very often, even when code starts off cleanly written, that new requirements are added. Without much thought, it is easy to go into the areas affected and edit the classes that have already been written. (Very quickly violating the Open/Closed and Single Responsibility (SRP) principles.)

We will start with something simple here just to get a feel for the pattern itself. So, without further ado, let's take a look at some code. Let's say we have a class called Handler and its job is to handle some command given to the system. We will avoid the use of generics for now to keep it simple.

namespace Patterns.Decorator
{
    public abstract class Handler
    {
        public abstract void Handle (object command);
    }
}

And let's say we have some concrete implementations of Handler that each talk to the database to update some record. (Null checks avoided to simplify the code.)

namespace Patterns.Decorator.Concrete
{
    public class FooUpdater : Handler
    {
        public override void Handle (object command)
        {
            // Foo is just some made-up class to give this method a bit of a body
            if (command is Foo)
            {
                var asFoo = (Foo)command;
                var record = database.Load(asFoo.Id) // get a record from the database
                record.Bar = asFoo.Bar;              // update the record
                database.Save(record);               // and save it
            }
        }
    }

    public class BazUpdater : Handler
    {
        public override void Handle (object command)
        {
            // Baz is just some made-up class, as well
            if (command is Baz)
            {
                var asBaz = (Baz)command;
                var record = // Okay, pretty much the same thing as above
                ...
            }
        }
    }
}

Everything is working well, but this was quickly implemented to get it in front of the customer. The functionality has been "blessed" and we realize we need to log exceptions. What I have seen, all too often, is something such as:

public class FooUpdater : Handler
{
    public override void Handle (object command)
    {
        try
        {
            if (command is Foo)
            {
                var asFoo = (Foo)command;
                var record = database.Load(asFoo.Id) // get a record from the database
                record.Bar = asFoo.Bar;              // update the record
                database.Save(record);               // and save it
            }
        }
        catch (Exception ex)
        {
            ErrorLogger.LogException(ex);
        }
    }
}

For a very simple system, this might work just fine. But this is not a maintainable solution. We have just broken the Open/Closed principle along with SRP. Our Handle method has taken a dependency on ErrorLogger (good luck testing this method now) and has to be changed every time we want to add some functionality. Even worse, that same boiler-plate code has to be copy-and-pasted all through our project. Yuck!

One way to fix this is to rewrite the Handler class:

public abstract class Handler
{
    protected abstract void HandleImpl (object command);

    public void Handle (object command)
    {
        try
        {
            this.HandleImpl(command);
        }
        catch(Exception ex)
        {
            ErrorLogger.LogException(ex);
        }
    }
}

But then our base class becomes flooded with all sorts of mixed behaviors (it tends to become a god object) and quickly becomes a maintenance nightmare. Imagine if we wanted to add some functionality that logged some information based on the type of command given. We would probably find ourselves with a nasty, brittle, switch statement. Once it was written, we would never want to add functionality because it would be too scary. You can't even test this mess to get some sort of regression tests without a bunch of pain. (No wonder some people don't like writing unit tests!)

Fear not! The Decorator Pattern is here to help save us from this unwieldy jumble of code. Let's think of each piece of functionality that we were trying to implement. They sort of wrap around each other and form a ball, kind of like decorating a cake. One layer of frosting, or functionality, at a time. We can easily achieve this same effect by pulling out the layers into their own classes that look like the original base class. But, instead of having a default constructor, they will take in a Handler, so we can layer functionality.

namespace Patterns.Decorator.Concrete
{
    public class FooUpdater : Handler
    {
        public override void Handle (object command)
        {
            if (command is Foo)
            {
                ... // Do the work like we originally did, above.
            }
        }
    }
}

namespace Patterns.Decorator.Logging
{
    public class Logger : Handler
    {
        private readonly Handler _decorated;

        public override void Handle (object command)
        {
            try
            {
                _decorated.Handle(command)
            }
            catch (Exception ex)
            {
                ErrorLogger.LogException(ex);
            }
        }

        public Logger(Handler decorated)
        {
            _decorated = decorated;
        }
    }
}

Now to add the logging functionality, we can leave the original FooUpdater class alone and just wrap it with new functionality. Easy as cake!

var handler = new Logger(new FooUpdater());

So, we have seen how to add functionality to a system without modifying the code we wrote originally. Of course, requirements change and we might need to modify the original code—but we have crafted our solution so that the original code only needs to be modified for one reason (the business rules have changed). Using the decorator pattern we can safely maintain the Open/Closed principle and SRP. Now we won't be so afraid to add that next layer of functionality.

No comments:

Post a Comment