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.