Decorator Pattern using the example of a cached repository

The decorator pattern allows you to add (dynamically) behavior to an individual object without affecting the behavior. It helps you with the Single Responsibility principle (see SOLID principles). It also adheres the famous quote: "Favor composition over inheritance". So it's a nice way of avoiding subclassing and still add extended functionaility.

Typically you have at least 3 components: An interface, an concrete implementation and the decorator itself. The following picture will show you how they correlate to each other.

pattern

The decorator as well as the concrete implementation both implement the interface component. But the decorator also has the concrete implementation as a field. What this allows is very simple:

  • The outside world has no idea that we are a decorator, they will always see a interface component
  • We have extension points before and after the call of operation from theconcrete implementation

Cached Repository

Initially I talked about creating a cached repository. This is just one of many applications for that pattern. Another one would be logging. As we can intercept before and after the "real" call, we have the ability to log for example the request and the response given from our concrete implementation. Back to our caching repository.

The idea is simple: We have an interface IRepository which is our contract to the outer world. We need a implementation provided by class SlowRepository and the decorator class CachedRepository:

uml cached

Why SlowRepository? You will see in a minute. First the easy part: Our contract IRepository:

public interface IRepository  
{  
  public Task<Person> GetPersonByIdAsync(int id);  
  
  public Task SavePersonAsync(Person person);  
}

We will use exact this interface in our sample in combination with a DI-Container. We build a new Person object and put it into the repository. Afterwards we load from that exact some repository the person with the Id 1.

var person = new Person(1, "Steven Giesel");  
var repository = provider.GetRequiredService<IRepository>();  
await repository.SavePersonAsync(person);  
 
var stopwatch = Stopwatch.StartNew();  
await repository.GetPersonByIdAsync(1);  
Console.WriteLine($"First call took {stopwatch.ElapsedMilliseconds} ms");  
 
stopwatch.Restart();  
await repository.GetPersonByIdAsync(1);  
Console.WriteLine($"Second call took {stopwatch.ElapsedMilliseconds} ms");

Now before we can run this, we need a real implementation and now you will see why I called it SlowRepository:

public class SlowRepository : IRepository
{
    private readonly List<Person> _people = new();

    public async Task<Person> GetPersonByIdAsync(int id)
    {
        await Task.Delay(1000);
        return _people.Single(p => p.Id == id);
    }

    public Task SavePersonAsync(Person person)
    {
        _people.Add(person);
        return Task.CompletedTask;
    }
}

Everytime we call GetPersonByIdAsync we will wait 1 second before we return the object. Of course this is for demonstrational purposes. Now we will go the last bit: the decorator itself. I will show you the code first and go through afterwards:

public class CachedRepository : IRepository
{
    private readonly IMemoryCache _memoryCache;
    private readonly IRepository _repository;

    public CachedRepository(IMemoryCache memoryCache, IRepository repository)
    {
        _memoryCache = memoryCache;
        _repository = repository;
    }

    public async Task<Person> GetPersonByIdAsync(int id)
    {
        if (!_memoryCache.TryGetValue(id, out Person value))
        {
            value = await _repository.GetPersonByIdAsync(id);
            _memoryCache.Set(id, value);
        }

        return value;
    }

    public Task SavePersonAsync(Person person)
    {
        return _repository.SavePersonAsync(person);
    }
}
  • The CachedRepository also implements IRepository
  • It receives IMemoryCache from the DI-Container to do the caching for use
  • We also receive an IRepository from the DI-Container which we will use later. We don't know that this is SlowRepository
  • GetPersonByIdAsync checks the IMemoryCache if it has a value with the given id. If not, go to our IRepository and get the person via GetPersonByIdAsync. Store this in our cache and return the value. If the cache has this id, we can directly return the person from the cache. No need to go to the real repository at all!
  • SavePersonAsync just calls SavePersonAsync from the "real" repository. Of course we could populate the cache here as well. But I didn't want to 😄 ... also for the sake of demonstration.

Now let's recap here before we continue. Our decorator acts as a Proxy for the real type. With that we can control what happens right before and after the real call to the object. In our CachedRepository case, we use the before aspect to check whether or not a cache is populated with the asked object. If so return it from the cache, otherwise retrieve from the real implementation and populate our cache.

Results

Lets revisit our snippet from the top. What do you think is the output on the console?

var person = new Person(1, "Steven Giesel");
var repository = provider.GetRequiredService<IRepository>();
await repository.SavePersonAsync(person);

var stopwatch = Stopwatch.StartNew();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"First call took {stopwatch.ElapsedMilliseconds} ms");

stopwatch.Restart();
await repository.GetPersonByIdAsync(1);
Console.WriteLine($"Second call took {stopwatch.ElapsedMilliseconds} ms");

Prints:

First call took 1023 ms
Second call took 0 ms

So what happens on the first call:

  • First we enter our CachedRepository and check if the cache has an entry with the Id 1
  • As it does not have any object it calls the real SlowRepository to retrieve the item
  • This retrieving takes 1 second thanks to Task.Delay(1000) in SlowRepository
  • We put this item in our cache and return it to the caller

What happens on the second call:

  • We enter again our CachedRepository and check if the cache has an entry with the Id 1
  • But now we have an entry
  • Therefore we don't go to the SlowRepository and directly return our cached object

That is all the magic we used. We clearly divided the responsibilities and this approach is super unit testable! Now there is only one part missing. Connecting all the things together:

DI - Optional

In my example earlier with the stopwatch I showed that I only use IRepository. Also my CachedRepository only uses IRepository. Now when you want to use this with DI you have to connect all the dots on your own. As I used a console application I installed those two packages: Microsoft.Extensions.DependencyInjection for the DI-Container. This is the default when you create a new ASP.NET Core project and Microsoft.Extensions.Caching.Memory for the MemoryCache.

Here the code for the DI-Container:

var provider = new ServiceCollection()
    .AddMemoryCache()
    .AddScoped<SlowRepository>()
    .AddScoped<IRepository>(p =>

    {
        var memoryCache = p.GetRequiredService<IMemoryCache>();
        var repository = p.GetRequiredService<SlowRepository>();
        return new CachedRepository(memoryCache, repository);
    })
    .BuildServiceProvider();
  • AddMemoryCache adds the IMemoryCache to the container
  • AddScoped<SlowRepository> adds the SlowRepository to the container
  • The next block says: "Hey let's register our CachedRepository as IRepository into the container. I have to resolve all the dependencies for a simple reason. My CachedRepository wants a IRepository as second parameter. But there is no object right now in the dependency graph which fulfills that. Therefore I have to do this manually.
  • Of course registering SlowRepository into the container and getting it out a line afterwards seems strange and I could just use return new CachedRepository(memoryCache, new SlowRepository()); but keep in mind that if your SlowRepository has a new constructor dependency you also have to adopt that piece of code here. Not so great.

Resources

As always you'll find this example on my github repository where I host the majority of my samples.

Epilog

Now it is quite debatable if the shown example is the decorator pattern or proxy pattern. From a structural point of view both are very very similar. It depends on your definition of those where you would classify the given example 😉

14
An error has occurred. This application may no longer respond until reloaded. Reload x