Dynamic Decorators
15 Oct 2022The decorator pattern is a powerful tool to apply “aspects” or cross cutting concerns such as logging or caching to a type without breaking the Single Responsibility Principle (SRP).
But sometimes creating and maintaining decorators with bigger interfaces or a bigger number of decorated types becomes a problem. Dynamic proxies can be a solution to this.
What is a decorator?
Imaging we have an interface like this
public interface IRepository
{
IReadOnlyCollection<Person> GetPersons();
void Update(Person person);
void Delete(Person person);
}
and an implementation which connects to a SQL database called SqlRepository
.
Let’s also imagine working directly with the database is too slow in some cases and
therefore we want to add some in-memory caching.
We could add the cache directly in the SqlRepository
but that would violate SRP and could
result in a mess easily if we add more and more SQL as well as caching related code.
Instead we will create a “decorator” like this:
public class RepositoryCachingDecorator : IRepository
{
private readonly IRepository myImpl;
private readonly ICache myCache;
public RepositoryCachingDecorator(IRepository impl, ICache cache)
{
myImpl = impl;
myCache = cache;
}
public IReadOnlyCollection<Person> GetPersons()
{
if (myCache.Items.Count == 0)
{
myCache.Add(myImpl.GetPersons());
}
return myCache.Items;
}
public void Update(Person person)
{
myCache.Update(person);
myImpl.Update(person);
}
public void Delete(Person person)
{
myCache.Delete(person);
myImpl.Delete(person);
}
}
Notice that the decorator “encapsulates” the actual IRepository
implementation, returns the
items from the cache once filled and updates cache and actual repository likewise.
Now we replace the creation of
var repository = new SqlRepository();
with
var repository = new RepositoryCachingDecorator(new SqlRepository(), cache);
which activates caching for all users of IRepository
without adding a single line of
code to SqlRepository
.
For more details on how to implement such a caching decorator watch this video:
This aspect of adding functionality without modifying existing types alone already makes the decorator
pattern powerful. But there is more.
Due to the way how decorators are constructed, we can compose multiple decorators
and so add more and more functionality to a type without modifying it.
For example, consider decorators adding logging and locking to the SqlRepository
var repository = new RepositoryCachingDecorator(
new RepositoryLockingDecorator(
new RepositoryLoggingDecorator(
new SqlRepository())), cache);
Dynamic decorators
The downside of the decorator pattern is that we have to implement the complete interface. This can become cumbersome if we want to decorate many different interfaces with the same aspect or the interfaces to decorate have a lot of Apis.
In such cases “dynamic proxies” can be a solution. In .NET we can use the DispatchProxy class to implement such a dynamic proxy based decorator.
A decorator adding locking to an IRepository
based on DispatchProxy
could look like this:
public class RepositoryLockingDecorator : DispatchProxy
{
private IRepository myImpl;
private object myLock;
public static IRepository Create(IRepository impl)
{
var proxy = Create<IRepository, RepositoryLockingDecorator>();
(RepositoryLockingDecorator)proxy).myImpl = impl;
return proxy;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
lock(myLock)
{
return targetMethod.Invoke(myImpl, args);
}
}
}
var repository = RepositoryLockingDecorator.Create(new SqlRepository());
As the CLR routes every call to any Api of IRepository
to the Invoke
method, we have
to implement the actual aspect - the locking - only once. With this approach, the size of
IRepository
is not relevant and we also do not have to adapt our decorator every time
IRepository
gets changed. Furthermore we could make the decorator generic and so enable it
to decorate any interface.
The downside of this approach is that, as it is based on reflection, it probably reduces performance a bit.
Conclusion
Whether manually or dynamically created, decorators are a powerful tool and should definitively considered more often when adding additional aspects to existing types. Dynamic proxies can be an approach to reduce the maintenance of such decorators.