Skip to main content

Decorator Pattern in .NET

· 4 min read
Zsolt Zabolai
Software Engineer | Co-Founder @ BuggyDevs

Design patterns are all around us. Even if we don't realize it, we might be using them, or even inventing new ones during our daily work. The Decorator design pattern is one such pattern. It can be useful for adding behavior to existing classes without modifying them.

Let's see an example where it can be useful.

The slow feature

It's Friday, almost the end of the day. You get an automated alert from your favorite monitoring system... the update you just deployed to production has made a critical feature very slow. Your boss is angry... yes still, he's the one that made you do a deployment on a Friday afternoon.

You just want to go home and start your weekend, but you can't let the system in this state, so you start looking.

The class you can't change

You quickly look at the code and realize that your colleague has added a new class with a method of 800 lines of code which is causing the issue.

You've never seen such code before, it does not make any sense... looks like AI generated a whole feature, without the proper oversight of a real person... You'll need to sit down to talk to your colleague, but he's gone for the weekend... this issue is yours to fix.

class MySlowFeature
{
public double ComputeData()
{
// 800 lines of code where something is slowing the system down...
}
}

That code is so scary, you don't want to touch it, so what can you do? You want to change the behavior of the class, without actually changing the code.

Turns out one pattern that can actually do this is called the Decorator design pattern.

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. https://refactoring.guru/design-patterns/decorator

You figure, you could use the decorator pattern to add a caching behavior to the class, without actually touching that mess.

Step one: Extract an interface

Preparing to implement the pattern, first you prepare the code by extracting an interface.

interface IMySlowFeature
{
double ComputeData();
}

class MySlowFeature : IMySlowFeature
{
public double ComputeData()
{
// 800 lines of code where something is slowing the system down...
}
}

Step two: Implement the decorator

Now, in your case the decorator will cache the result of the slow function. It doesn't actually solve the performance issue, but offers a good enough workaround until you can sort it out when your colleague returns next week.

// The decorator implements the same interface as the decorated (inner) class.
// It takes the decorated class in its constructor.
class MyCachingDecorator(IMySlowFeature inner, ICache cache) : IMySlowFeature
{
const string CacheKey = "MyCacheKey";

public double ComputeData()
{
// If the cache contains the result,
if (cache.Contains(CacheKey))
{
return cache.Get(CacheKey);
}

// Call the slow method and save its result to the cache for a limited amount of time.
var value = inner.ComputeData();
cache.Add(CacheKey, value, TimeSpan.FromHours(2));
return value;
}
}

Then, we can replace the references to MySlowFeature to the new interface we introduced IMySlowFeature, and after that the only thing that remains is registering the decorator in DI. There are multiple ways of doing that, but since our solution is just temporary until our colleague comes back, we can just use a simple factory function.

services.AddTransient<MySlowFeature>();
services.AddTransient<IMySlowFeature>(s => new MyCachingDecorator(s.GetRequiredService<MySlowFeature>(), s.GetRequiredService<ICache>()));

A much nicer way would be to use Scrutor, which makes it very easy.

services.AddTransient<IMySlowFeature, MySlowFeature>();
services.Decorate<IMySlowFeature, MyCachingDecorator>();

Now that's out of the way... it's time for you to enjoy your weekend! 😉

Conclusions

In conclusion, the Decorator design pattern allows us to change the behavior of a class, without actually changing its code.

Perhaps the example wasn't the most realistic one, but there are lots of cases where this pattern can help, either with separation of concerns, or even with changing behavior that otherwise would be impossible to change.

In what scenarios can the decorator pattern become useful for you? (Try out our new comment box below 😉)