KISS and Decorators

Decorators: a powerful and often misused design pattern

KISS and Decorators

posted in design on  •   • 

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.


Stuff that came into being during the making of this post
Tags: war-story