DEV Community

Dennis
Dennis

Posted on • Updated on

Design patterns in Umbraco | Decorator

If you are new to design patterns, I recommend you to read part 1 of this series before diving into this post. It will help you understand the fundamental concepts of design patterns and prepare you for what I will be discussing here.

In this post I'd like to introduce you to the decorator pattern.

Decorator pattern

this pattern must be my second favourite design pattern out there. It's a very simple and elegant pattern that opens up an incredible amount of possibilities. Umbraco is an excellent example for usage of the decorator pattern.

You'll want to use the decorator pattern if you need to add new features on top of existing types without affecting the original implementation. The pattern looks like this:

UML diagram of the decorator pattern

The diagram shows a service that implements an interface and a decorator that implements the same interface, but also aggregates that interface.

Since Umbraco uses dependency injection, it's very easy to use the decorator pattern wherever needed. Let's have a look at how it works:

Decorator pattern in practice

Let's say you want to measure the performance of your blog search:

public class BlogService
{
    private IUmbracoContextAccessor _umbracoContextAccessor;

    // Constructor omitted for brevity

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        var results = _umbracoContextAccessor
            .GetRequiredUmbracoContext()
            .Content.GetAtRoot()
            .First()
            .Descendants<BlogArticle>()
            .Skip(skip)
            .Take(take)
            .ToList();

        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

You could simply change the class to this:

public class BlogService
{
    private IUmbracoContextAccessor _umbracoContextAccessor;
    private IProfiler _profiler;

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        using (_profiler.Step("Search blogs"))
        {
            var results = _umbracoContextAccessor
                .GetRequiredUmbracoContext()
                .Content.GetAtRoot()
                .First()
                .Descendants<BlogArticle>()
                .Skip(skip)
                .Take(take)
                .ToList();

            return results;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, for the sake of this example, let's say you need to publish events every time this service is used:

public class BlogService
{
    private IUmbracoContextAccessor _umbracoContextAccessor;
    private IProfiler _profiler;
    private IEventAggregator _publisher;

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        _publisher.Publish(new BeginSearchEvent(/* parameters */));

        IEnumerable<BlogArticle> results;
        using (_profiler.Step("Search blogs"))
        {
            results = _umbracoContextAccessor
                .GetRequiredUmbracoContext()
                .Content.GetAtRoot()
                .First()
                .Descendants<BlogArticle>()
                .Skip(skip)
                .Take(take)
                .ToList();
        }

        _publisher.Publish(new EndSearchEvent(/* parameters */));

        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

Although this works, you can clearly see that this service is growing out of control. The service started out simple, but ended up with three responsibilities. Let's take a step back and use the decorator pattern instead.


We'll start again with the BlogService, but now it implements an interface:

public interface IBlogService
{
    IEnumerable<BlogArticle> Get(int skip, int take);
}

public class BlogService
    : IBlogService
{
    private IUmbracoContextAccessor _umbracoContextAccessor;

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        var results = _umbracoContextAccessor
            .GetRequiredUmbracoContext()
            .Content.GetAtRoot()
            .First()
            .Descendants<BlogArticle>()
            .Skip(skip)
            .Take(take)
            .ToList();

        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we'll create a decorator for profiling:

public class DecoratorBlogServiceProfiler
    : IBlogService
{
    private IBlogService _decoratee;
    private IProfiler _profiler;

    // Notice that this type takes an IBlogService in the constructor, but also implements the interface
    public DecoratorBlogServiceProfiler(IBlogService decoratee, IProfiler profiler)
    {
        _decoratee = decoratee;
        _profiler = profiler;
    }

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        using (_profiler.Step("Search blogs"))
        {
            return _decoratee.Get(skip, take);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's also immediately create the decorator that publishes events:

public class DecoratorBlogServiceEvents
    : IBlogService
{
    private IBlogService _decoratee;
    private IEventAggregator _publisher;

    public DecoratorBlogServiceEvents(IBlogService decoratee, IEventAggregator publisher)
    {
        _decoratee = decoratee;
        _publisher = publisher;
    }

    public IEnumerable<BlogArticle> Get(int skip, int take)
    {
        _publisher.Publish(new BeginSearchEvent(/* parameters */));

        var result = _decoratee.Get(skip, take);

        _publisher.Publish(new EndSearchEvent(/* parameters */));

        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now to tie it all together, we need to register the decorators in the dependency injection container. ASP.NET 6 does not support the decorator pattern out-of-the-box, so you'll have to install a NuGet package for it. In this example, I use Scrutor:

public class BlogComposer : IComposer
{
    public void Composer(IUmbracoBuilder builder)
    {
        // This is the registration of the original service.
        //    The decorator works regardless of how you register your service.
        builder.Services.AddScoped<IBlogService, BlogService>();

        // Scrutor adds the following extension:
        builder.Services.Decorate<IBlogService, DecoratorBlogServiceProfiler>();
        builder.Services.Decorate<IBlogService, DecoratorBlogServiceEvents>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep in mind that the order is important for decorators! The decorator that you register first, will execute last.

The benefits

The code is now better in several ways:

  • Testability: We can test each feature individually. We can test profiling without testing the events and we can test the blog service without the profiling.
  • Readability: We can see exactly what the blog service does without the overhead of the additional features.
  • Scalability: It may take an extra step or two to add a new decorator, but we no longer have a single type that grows beyond its original purpose
  • Flexibility: We have easy control over the order in which the different decorators execute, simply by switching them around in the composer. No more need to put things inside or outside of brackets. We can also easily disable any decorator whenever we want. For example if some feature turns out to have some security vulnerability, one can easily turn the feature off by commenting out the decorator in the composer.

This is just one example of many. Some other ways in which I have used the decorator pattern include:

  • ✅ Preprocessing user input in an api controller so that the business logic can work with a uniform model.
  • ✅ Adding caching layers to services.
  • ✅ Controlling input values in an Umbraco service that I otherwise couldn't override.
  • ✅ Isolating technical debt caused by sub-standard solutions in moments when no better alternative is available.

When you have a hammer...

Once again, keep in mind that design patterns are not rules, but guidelines. A general rule that I use for the decorator pattern is that the logic of the decorator should not be part of the core business logic. That is: you should be able to remove the decorator without breaking the main flow of your code. Some bad example usages of decorator pattern are:

  • ❌ Verifying a license on your Umbraco plugin in combination with dependency injection: A developer could simply remove your decorator from the DI container and use your software without license. You can still use the decorator pattern, but you should apply your decorator before registering your dependency.
  • ❌ Calling different implementations of a similar service. You'll want to use the strategy pattern for that.
  • ❌ The individual phases of a multi-phase process.

Conclusion

The decorator pattern is a powerful tool to have in your toolbelt. When used properly, it can greatly improve the quality of your code. With this power comes some responsibility though as it can be quite easy to overuse the pattern.

That's all there is for now! Have you ever used the decorator pattern? What was your use case? What are your thoughts on this design pattern? Let me know with a comment! Any feedback on my writing style is also greatly appreciated. Thanks for reading! 😊

Top comments (0)