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.
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
:
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 implementsIRepository
- 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 isSlowRepository
GetPersonByIdAsync
checks theIMemoryCache
if it has a value with the given id. If not, go to ourIRepository
and get the person viaGetPersonByIdAsync
. 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 callsSavePersonAsync
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)
inSlowRepository
- 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 theIMemoryCache
to the containerAddScoped<SlowRepository>
adds theSlowRepository
to the container- The next block says: "Hey let's register our
CachedRepository
asIRepository
into the container. I have to resolve all the dependencies for a simple reason. MyCachedRepository
wants aIRepository
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 usereturn new CachedRepository(memoryCache, new SlowRepository());
but keep in mind that if yourSlowRepository
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 😉