One of my favorite design pattern is the Decorator pattern. It's an elegant way to compose behavior without changing existing code. It's non-intrusive and makes the separation of concerns very clear between each class. Everything is then very easy to test and reuse.
So what's the problem?
The pain point I always have with this pattern is to create a long chain of decorator in a clean and simple way. It always ends up looking like this or even worst:
new LoggerDecorator(new ErrorHandlerDecorator(new CacheDecorator(new MyService())));
Moreover, this needs to be repeated every time I need MyService
.
Solutions!
Hopefully, there are some solutions out there to tackle this specific problem. One of them is to use a DI framework like Autofac. Autofac already provides out of the box a nice construct to register decorators in your container and will make sure to resolve your decorator chain at runtime.
Extract from Autofac's documentation
var builder = new ContainerBuilder();
// Register the services to be decorated. You have to
// name them rather than register them As<ICommandHandler>()
// so the *decorator* can be the As<ICommandHandler>() registration.
builder.RegisterType<SaveCommandHandler>()
.Named<ICommandHandler>("handler");
builder.RegisterType<OpenCommandHandler>()
.Named<ICommandHandler>("handler");
// Then register the decorator. The decorator uses the
// named registrations to get the items to wrap.
builder.RegisterDecorator<ICommandHandler>(
(c, inner) => new CommandHandlerDecorator(inner),
fromKey: "handler");
var container = builder.Build();
// The resolved set of commands will have two items
// in it, both of which will be wrapped in a CommandHandlerDecorator.
var handlers = container.Resolve<IEnumerable<ICommandHandler>>();
Well... this is a bit better as we only have to register our chain of decorator once. However, the registration part is still hard to understand. Nesting a few builder.RegisterDecorator
gets complex real fast.
The simplified approach
What I would like is a simple interface to get rid of most of the boiler plate Autofac code lines required for the builder.RegisterDecorator
.
Something like this
builder.RegisterTypeWithDecorator<IMyService, MyService>()
.WithDecorator<CacheDecorator>()
.WithDecorator<ErrorHandlerDecorator>()
.WithDecorator<LoggerDecorator>()
.CompleteDecoratorRegistration();
Haaaaa, it feels much better and cleaner. Now let's try to implement this.
NOTE: The next part will be complicated, but it's ok. The goal is to abstract that complexity away, so we can use the nice methods above.
Interface
public interface IRegistrationDecoratorBuilder<TInterface, TImplementation>
{
IRegistrationDecoratorBuilder<TInterface, TImplementation> WithDecorator<TDecorator>()
where TDecorator : TInterface;
// The return type is quite ugly, but expected by Autofac
IRegistrationBuilder<TInterface, LightweightAdapterActivatorData, DynamicRegistrationStyle> CompleteDecoratorRegistration();
}
Implementation
internal class RegistrationDecoratorBuilder<TInterface, TImplementation>
: IRegistrationDecoratorBuilder<TInterface, TImplementation>
{
private readonly ContainerBuilder _builder;
private readonly IList<Type> _decoratorTypes = new List<Type>();
public RegistrationDecoratorBuilder(ContainerBuilder builder)
{
this._builder = builder;
}
public IRegistrationDecoratorBuilder<TInterface, TImplementation> WithDecorator<TDecorator>()
where TDecorator : TInterface
{
this._decoratorTypes.Add(typeof(TDecorator));
return this;
}
public IRegistrationBuilder<TInterface, LightweightAdapterActivatorData, DynamicRegistrationStyle>
CompleteDecoratorRegistration()
{
var registrationNameToDecorate = this.RegisterConcreteImplementationToDecorate();
registrationNameToDecorate = this.GetDecoratorTypeExceptTheTopMost()
.Aggregate(
registrationNameToDecorate,
(current, decoratorType) => this.RegisterDecorator(decoratorType, current));
return this.RegisterTopMostDecorator(registrationNameToDecorate);
}
private IEnumerable<Type> GetDecoratorTypeExceptTheTopMost()
{
return this._decoratorTypes.Take(this._decoratorTypes.Count - 1);
}
private string RegisterConcreteImplementationToDecorate()
{
var registrationNameToDecorate = nameof(TImplementation);
this._builder.RegisterType<TImplementation>().Named<TInterface>(registrationNameToDecorate);
return registrationNameToDecorate;
}
private string RegisterDecorator(Type decoratorType, string registrationNameToDecorate)
{
var decoratorTypeName = decoratorType.Name;
this._builder.RegisterType(decoratorType).Keyed(decoratorTypeName, decoratorType);
this._builder.RegisterDecorator<TInterface>(
(context, inner) => (TInterface)context.ResolveKeyed(
decoratorTypeName,
decoratorType,
new TypedParameter(typeof(TInterface), inner)),
registrationNameToDecorate,
decoratorTypeName);
registrationNameToDecorate = decoratorTypeName;
return registrationNameToDecorate;
}
private IRegistrationBuilder<TInterface, LightweightAdapterActivatorData, DynamicRegistrationStyle>
RegisterTopMostDecorator(string registrationNameToDecorate)
{
var lastDecoratorType = this._decoratorTypes.Last();
this._builder.RegisterType(lastDecoratorType).Keyed(lastDecoratorType.Name, lastDecoratorType);
return this._builder.RegisterDecorator<TInterface>(
(context, inner) => (TInterface)context.ResolveKeyed(
lastDecoratorType.Name,
lastDecoratorType,
new TypedParameter(typeof(TInterface), inner)),
registrationNameToDecorate);
}
Extension method
public static IRegistrationDecoratorBuilder<TInterface, TImplementation> RegisterTypeWithDecorator<TInterface,
TImplementation>(this ContainerBuilder builder)
where TImplementation : TInterface
{
return new RegistrationDecoratorBuilder<TInterface, TImplementation>(builder);
}
Conclusion
Voilà! We abstracted all the complexity away with the magic of c# extension methods, so our new decorator builder adds up seamlessly to the Autofac container builder.