[go: up one dir, main page]

DEV Community

Cover image for Design Patterns: Decorator
Russ Hammett
Russ Hammett

Posted on • Originally published at blog.kritner.com on

Design Patterns: Decorator

The decorator pattern is a structural design pattern that can be used to add functionality to classes without modifying the original class or its interface.

The Decorator Pattern

From Wikipedia:

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern. The decorator pattern is structurally nearly identical to the chain of responsibility pattern, the difference being that in a chain of responsibility, exactly one of the classes handles the request, while for the decorator, all classes handle the request.

“Prefer composition over inheritance” might be something you’ve heard of before, and I feel like the decorator pattern is basically the epitome of this saying. Decorators are themselves implementations of an abstraction, that also depend on the abstraction itself. This allows for “composable” pieces of behavior in that you would likely have a single “main” implementation of an abstraction, then separate “decorator” implementations that build on top of it.

I’m not sure if that made sense, so hopefully an example will help clear things up.

Implementing the decorator pattern

The abstraction

For our abstraction, we’re going to create a service that retrieves the weather for us:

public interface IWeatherGettinator
{
    Task<Weather> GetWeather();
}

public class Weather
{
    public Guid Id { get; }
    public int FreedomUnits { get; set; }
    public bool ItGonRain { get; set; }

    public Weather()
    {
        Id = Guid.NewGuid();
    }

    public override string ToString()
    {
        var sb = new StringBuilder()
            .AppendLine("The current weather:")
            .AppendLine($"\tId: {Id}")
            .AppendLine($"\tTemperature (in freedom units): {FreedomUnits}")
            .AppendLine($"\tIt gon rain? {ItGonRain}");

        return sb.ToString();
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above, we have an interface IWeatherGettinator that has a single method, which takes no arguments, and returns the weather; simple enough.

The base implementation

For this example, we’re going to pretend that to get the weather is an expensive operation, just because it makes one of the decorators of IWeatherGettinator more interesting and easier to see the point of it all (IMO).

So for the implementation of the IWeatherGettinator:

public class WeatherGettinator : IWeatherGettinator
{

    private readonly Random _random;

    public WeatherGettinator(Random random)
    {
        _random = random;
    }

    /// <summary>
    /// Gets the weather, mimics a long running task.
    /// </summary>
    /// <returns><see cref="Task"/> of <see cref="Weather"/></returns>
    public async Task<Weather> GetWeather()
    {
        await Task.Delay(5000);

        return new Weather()
        {
            FreedomUnits = _random.Next(0, 100),
            ItGonRain = _random.Next(0, 1) == 1
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

On getting the weather (which is a ~5 second operation), we get a random temperature and a value that indicates if it’s going to rain or not. Each time GetWeather is invoked, it takes another 5 seconds to retrieve a random “weather” instance.

The above implementation should look something like this when you ToString the retrieved Weather via a GetWeather invoke:

Weather Gettinator Output

Our first decorator

Getting the weather seems to take a pretty long time! Perhaps we can introduce our first decorator to get some timing information on the implementation of IWeatherGettinator:

public class StopWatchDecoratorWeather : IWeatherGettinator
{
    private readonly IWeatherGettinator _weatherGettinator;

    public StopWatchDecoratorWeather(IWeatherGettinator weatherGettinator)
    {
        _weatherGettinator = weatherGettinator;
    }

    public async Task<Weather> GetWeather()
    {
        Stopwatch sw = Stopwatch.StartNew();
        var weather = await _weatherGettinator.GetWeather();
        sw.Stop();

        Console.WriteLine($"Decorated IWeatherGettinator ran for {sw.ElapsedMilliseconds}ms");
        return weather;
    }
}
Enter fullscreen mode Exit fullscreen mode

This might look a little strange, so let’s go over it a bit. The StopWatchDecoratorWeather is an implementation of IWeatherGettinator that also depends on an implementation of IWeatherGettinator. You can see that the method implementation GetWeather starts a stop watch, calls the injected implementation of IWeatherGettinator, stops the stopwatch, and writes to the console the length of time the operation took.

Now you probably wouldn’t actually write a decorator exactly like the above, but you could do something like it, especially if you were to make use of a logging framework. Logging the above could make more sense, especially if the invoke took more than a certain amount of time.

How does the above get used? Well, you could “compose” your object like so:

IWeatherGettinator weatherGettinator = new StopWatchDecoratorWeather(new WeatherGettinator(new Random()));
Enter fullscreen mode Exit fullscreen mode

You would actually want to be constructing these objects via an IOC container or something similar, this was to just get the point across… also there is some complexity to registering decorated objects with at least the .net core built in IOC container; though you could also use a factory to accomplish it more easily.

What does the above construction mean? Well, we’re instantiating a new instance of a StopWatchDecoratorWeather which itself depends on a IWeatherGettinator instance, in this case we’re passing in a concrete implementation of WeatherGettinator.

Running the IWeatherGettinator while making use of the StopWatchDecoratorWeather would look like this:

StopWatchDecoratorWeather Output

The issue with inheritance

Inheritance has an issue; an issue that can be solved through composition. I’ve mentioned this several times now, but it’s kind of hard (at least for me) to convey what I mean.

Say we wanted to introduce another piece of functionality; in this post we’re going to be doing it through the use of decorators, but just imagine for a moment that we weren’t.

In c# a class can only ever extend a single other class. If we ever wanted to “mix and match” behaviors of an abstraction that relied on inheritance, that can be extremely difficult to do. If we have an interface IFoo, with an implementation Foo, then had separate implementations A and B, both of which extended Foo, how would we go about introducing yet another implementation C, that needed some of its own unique characteristics, as well as the additional functionality provided by both A and B? This would be challenging with an inheritance scenario, but is really trivial when making use of decorators.

To demonstrate that, let’s introduce another decorator to our IWeatherGettinator.

The second decorator

We’ve done some testing with our StopWatchDecoratorWeather and determined that we could save a lot of time getting the weather if we introduced some caching. Thankfully, introducing caching via a decorator is very simple, and should look pretty similar to our first decorator!

public class CachedWeatherGettinator : IWeatherGettinator
{
    private readonly IWeatherGettinator _weatherGettinator;

    private Weather _cachedWeather;
    private DateTime _cachedOn;

    public CachedWeatherGettinator(IWeatherGettinator weatherGettinator)
    {
        _weatherGettinator = weatherGettinator;
    }

    public async Task<Weather> GetWeather()
    {
        var now = DateTime.Now; // this should be injected for testability, but we're demonstrating a decorator so...
        var timeDiffSecondsSinceCachedValue = now.Subtract(_cachedOn).TotalSeconds;

        if (_cachedWeather == null || timeDiffSecondsSinceCachedValue > 30)
        {
            Console.WriteLine("Cached value does not exist or is expired, get a new one.");
            _cachedOn = now;
            _cachedWeather = await _weatherGettinator.GetWeather();
        }
        else
        {
            Console.WriteLine("Using cached value...");
        }

        return _cachedWeather;
    }
}
Enter fullscreen mode Exit fullscreen mode

In our new CachedWeatherGettinator, we’re making use of some instance state to return the previously retrieved weather if it’s less than 30 seconds since it was retrieved. You’ll notice this implementation, like our first decorator, both implements and depends on IWeatherGettinator.

We can now try out our new decorator like this:

IWeatherGettinator weatherGettinator = new CachedWeatherGettinator(new WeatherGettinator(new Random()));
Enter fullscreen mode Exit fullscreen mode

And you’ll see that the “first” call to the IWeatherGettinator will take the 5 seconds, but another call made immediately after will return much faster.

But even more interesting that that, is we can make use of multiple decorators for the same abstraction, the construction of which is considered composition.

What could that look like?

IWeatherGettinator weatherGettinator = new StopWatchDecoratorWeather(new CachedWeatherGettinator(new WeatherGettinator(random)));
Enter fullscreen mode Exit fullscreen mode

We’re “composing” our object by decorating our base WeatherGettinator with multiple decorators! We have decorated the base object with both a StopWatch as well as Caching layer, mostly to demonstrate that the caching layer is in fact working. Let’s take a look!

StopWatch and Caching Decoration

Reasons to use this pattern

Hopefully it should be pretty clear after going through the post why this pattern can be quite powerful, but just to reiterate:

  • Adding additional functionality to an abstraction without changing the abstraction or its concretions.
  • Keeping concerns of caching or logging separate from the base implementation. These are taken care of via decorators, allowing your core “business logic” to stay pure, and uncaring about the functionality provided by the decorators.

References

Top comments (0)