KISS and Decorators
posted in design on • by Wouter Van Schandevijl •I picked up a UserStory to make a small adjustment.
The story was simple enough as was the required change but… Looking at the code, how was it supposed to work? It looked like it just.. Shouldn’t…
Took some time but I found out, the behavior of the software was defined in the IOC registration, at startup, with Autofac.
The Implementation
A simplified version of the code would be:
public interface IDoer
{
void DoIt();
}
public class Doer : IDoer
{
public void DoIt()
{
// Actual Business Logic here
// But... Only happening for
// one case?
}
}
And then another implementation, a decorator:
public class LoopingDoer : IDoer
{
private readonly IDoer _inner;
public LoopingDoer(IDoer inner)
{
_inner = inner;
}
public void DoIt()
{
// We were looping over something
// that made sense here, not just
// this for loop:
for (int i = 0; i < 10; i++)
{
_inner.DoIt();
}
}
}
The Magic
To make it all work, the “magic” ✨🦄 Autofac wiring:
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<Doer>().As<IDoer>();
containerBuilder.RegisterDecorator<LoopingDoer, IDoer>();
Probably there is no one in your team who knows what this does exactly and they’d have to look at a completely different location (app startup) before it makes any sense.
Keep it Simple
After removing the decorator and the Autofac registration, we ended up with:
public class Doer : IDoer
{
public void DoIt()
{
for (int i = 0; i < 10; i++)
{
// Business Logic here
// New UseCase example
if (i == 0)
{
// UserStory implementation here
}
}
}
}
Which is less code and much easier to understand, and it also made it easy, trivial even, to add the required change.
Decorator Pattern
An excellent design pattern to adhere to the Open/Closed Principle: Add functionality without modification.
Intent
It’s one of the Structural Patterns from the GoF.
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Applicability
When extension by subclassing is impractical because a large number of independent extensions are possible and would produce an explosion of subclasses to support every combination.
Consequences
More flexible than static inheritance:
Responsibilities can be added and removed at run-time whereas inheritance
requires creating a new class for each new combination.
Avoids feature-laden classes:
Instead of trying to support all features in a complex, customizable class,
you define a simple class and add functionality incrementally with Decorator
objects.
Lots of little objects:
The objects differ only in the way they are interconnected. Although easy to
customize, you can no longer follow the execution flow from the source code
alone and they can be hard to debug.
Known Uses
I/O Streams are typically implemented using Decorators and they are a really good fit for the pattern too!
byte[] buffer = "test"u8.ToArray();
using (FileStream fileStream = new(path, FileMode.Create, FileAccess.Write))
using (BufferedStream bufferedStream = new(fileStream))
using (GZipStream gzipStream = new(bufferedStream, CompressionMode.Compress))
{
gzipStream.Write(buffer, 0, buffer.Length);
}
We cleanly avoid having to create a GZipBufferedFileStream
for this particular
case. Having to write all these implementations would result in said explosion
of subclasses and also make it difficult to keep things DRY!
Conclusion
Using the decorator pattern
instead of writing a simple foreach
loop;
it’s just adding accidental complexity.
You end up with the consequences of the design pattern (hard to learn and debug) without any of the benefits.
Keep It Simple.