Decorator Pattern : *Werey Dey Disguise*

Decorator Pattern : *Werey Dey Disguise*

ยท

6 min read

Inheriting legacy codebases is just hell ๐Ÿคฆโ€โ™‚๏ธ, you don't have full knowledge of all the moving pieces, and then you find code scattered around that you have exactly no idea what they do, and there's the little whisper in your ears telling you "if you touch what you shouldn't, say bye-bye to your weekend plans". Despite this, you're required to add some new functionality to an existing piece within the system and you have to get it done asap, what can you do?

Well, you can dive in and study that specific part of the codebase - which would most likely have some dependencies on other parts of the system - with the aim to find the best place to modify this functionality and get it over with, what could go wrong? ๐Ÿคทโ€โ™€๏ธ .... ๐Ÿž๐Ÿž๐Ÿž๐Ÿž๐Ÿž. Asides breaking the Open-Closed principle that states:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

and also the Single Responsibility Principle that states:

A class should have only one reason to change.

You could introduce a lot of bugs into the system and you definitely do not want to do that. So how do you solve this problem?

The Decorator Pattern (Wrapper)

Our dear friends from the Gang of Four to the rescue again!. With a pattern that allows you to wrap around an existing piece of functionality using a decorator, affording you a safer way to extend functionality without directly modifying previously existing code ๐ŸŽ‰๐ŸŽ‰.

Getting down to it ๐Ÿ‘ฉโ€๐Ÿ’ป

Problem

Imagine with me, a monster classCountriesProvider within this code base with crazy SQL queries to a database to provide a list of countries, and you are tasked with the functionality of adding caching capabilities.

 public class CountriesProvider
    {
        public List<Country> GetCountries()
        {
            //some crazy 300 lines of magic...
            return countries;
        }
    }
  public class CountriesController : Controller
    {
        public ICountriesProvider _countriesProvider;
        public CountriesController(ICountriesProvider countriesProvider)
        {
            _countriesProvider = countriesProvider;
        }
        public IActionResult GetCountry()
        {
            return Ok(_countriesProvider.GetCountries());   

        }
    }

Our Solution

Using the Decorator Pattern, we:

  • Find a way to create an abstraction of the concrete implementation. This could be achieved using an abstract class or an interface. Using an interface results in a ICountriesProvider interface:
public interface ICountriesProvider
    {
        List<Country> GetCountries();
    }
 public class CountriesProvider : ICountriesProvider
    {
        public List<Country> GetCountries()
        {
            //some crazy 300 lines of magic...
            return countries;
        }
    }
  • Create our Decorator class. The Decorator(wrapper) must implement this extracted interface and contain the object that is to be extended ( this is achieved here by a parameterized constructor).
    public class CacheableContriesProvider : ICountriesProvider
    {
        private readonly ICountriesProvider _provider;
        public CacheableContriesProvider(ICountriesProvider countryProvider)
        {
            _provider = countryProvider;
        }
        public List<Countries> GetCountries()
        {
            //A default implementation is required.
            return _provider.GetCountries(); 
        }
    }

Note that the parameter to the CacheableCountriesProvider constructor is not a concrete type (programming against an interface), this allows the constructor accept any concrete type that implements the ICountriesProvider interface, this type could even be another decorator allowing us to create several layers of wrappers.

In the CacheableCountriesProvider, we can implement the caching functionality required. In this example, I'll be using asp.net core's IMemoryCache:

public class CacheableContriesProvider : ICountriesProvider
    {
        private readonly ICountriesProvider _provider;
        private readonly IMemoryCache _cache;
        private readonly string Cachekey = "CountryList";
        public CacheableContriesProvider(ICountriesProvider countryProvider, IMemoryCache cache)
        {
            _provider = countryProvider;
            _cache  = cache;
        }
        public List<Country> GetCountries()
        {
            if (_cache.TryGetValue<List<Country>(Cachekey, out var countries))
            {
                return countries;
            }
            else
            {
                var countries =  _provider.GetCountries();
                _cache.Set(Cachekey, countries);
                return countries;
            }
        }
    }
  • Now that we have safely added a caching wrapper, we need to replace the instance of the provider that is being used in our asp.net core web application.
 public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache();
        //services.AddScoped<ICountriesProvider, CountriesProvider>();
        services.AddScoped<ICountriesProvider>(serviceProvider =>
        {
            var cache = serviceProvider.GetService<IMemoryCache>();
            ICountriesProvider countriesProvider = new CountriesProvider();
            ICountriesProvider withCaching = new CacheableCountriesProvider(countriesProvider, cache);
            return withCaching;
        })
    }

The injected instance to the CountriesController is the CacheableCountriesProvider and when the call is made in the GetCountries() action, the execution is passed to the decorator first, and would only proceed into the CountriesProvider if the cache is empty (also note there was no need to touch the CountriesController).

We've successfully added caching and adhered to both Open-Closed and Single Responsibility principles.

Weekend plans are still on ๐Ÿ˜‰๐Ÿ˜‰.

The Decorator pattern comes in handy when you need to implement additional functionality in an existing system whilst maintaining separation of concerns across these components.

What do you do when you're then asked to add a performance logging functionality on top of this existing system?

You can decorate it!!! ๐Ÿ›€๐Ÿ›€.