How to Dynamically Add Behaviors With Decorator Pattern in C#
Everything you need to know about Decorator Pattern in one place.
What is the Decorator Pattern
The Decorator Pattern is a structural design pattern that allows you to dynamically add behavior to a class without making changes to that class. In other words, it uses composition instead of inheritance to add the functionality of an object.
IComponent
IComponent
is an interface
. It is an abstraction of what we want to decorate. You can also use an abstract class
.
Component
The Component
is a class
that you want to add behavior. The component needs to implement an interface
or extend an abstract class
.
Decorator
The Decorator class
will implement the same interface
or abstract class
as the Component class
.
The decorator’s constructor always takes an object implementing the same interface
as a parameter. Then the object of Component
is introduced into the class
attribute in Component’s constructor. Like this.
public DecoratorOne(IComponent component)
{
_component = component;
}
When Decorator implements an interface IComponent, you must implement methods SomeMethod()
and AnotherMethod()
. Into those methods, you can add new behavior.
public void SomeMethod()
{
//Decorating before inner component method invocation
_component.SomeMethod();
//Decorating after inner compoenent method invocation
}
Multiple Decorators
You are not limited to just one Decorator object per Component class. You can define Decorator which decorates another Decorator which decorates Components and so on. Look at the image below.
Both pictured Decorators must implement both methods defined in the IComponent interface.
Block named Code represents a place in the codebase where I am operating with Component.
The code will always invoke methods of the most outer Decorator firstly. To bubbled into a Component class, you must call the same-titled method of an inner Decorator in every Decorator.
If you want to use new behaviors of both Decorators at the same time, you must change the initialization of the Component object in the calling code. It can be done in three steps.
- Initialization of
DecoratorTwo
. - Initialization of
DecoratorOne
and passing into the constructor ofDecoratorTwo
. - Initialization of Component and passing into the constructor of
DecoratorOne
.
That’s it! You don’t have to modify any more code. The word dynamic gets its value.
Tutorial
Prerequisites
The tutorial assumes basic knowledge of .NET and Object-Oriented Programming.
Introduction of the Sample project
The sample project is a web application that collects data about the location of the Company's cars. An external API provides data.
Your Role at the Project
Imagine you are working as a support developer, and the Sample project is your responsibility. Suddenly, you have got a phone call from the customer;
Hello, Mr. AwesomeReader, I would like to report that the data location page ends with the timeout error.
Or maybe he is not that polite.
You are going to ask for advice from colleagues. They advise you to measure the time of how long it application takes to collect data.
LocationService class
LocationService class
is taking care of requesting an API and serializing collected data into Data-Transfer objects.
Class
implements interface ILocationService
. The interface
contains a definition of the method GetLocation()
. The implementation looks like this.
public class LocationService : ILocationService
{
private RestClient _apiClient;
private string _apiKey;
public LocationService(String apiKey)
{
_apiKey = apiKey;
_apiClient = new
RestClient("http://api.companycarlocations.org");
}
public List<LocationDto> GetLocations()
{
var request = new RestRequest("data/2.5/location");
request.AddParameter("appid", _apiKey);
var response = _apiClient.Get(request);
if (response.IsSuccessful)
{
return JsonConvert.DeserializeObject<List<LocationDto>>(response.Content);
}
else
{
throw new Exception();
}
}
}
Maybe you get an idea to add a logging code directly into the method. Let me tell you why it is a bad idea.
First, it will make the class a lot harder to read. More importantly, when you are modifying the existing code, you can quickly introduce an error to the application.
Another reason for not doing this is a violation of the Single Responsibility Principle. The meaning of principle can be expressed in one sentence.
A class should have only one reason to change.
Because you are a great developer and thanks to the great author (me), who tell you why is it a bad idea, you decide to use Decorator Pattern. Making that decision allows you to extend LocationService without modification of class. This reminds me of another SOLID principle and it is an Open-Closed Principle. The main thought of this principle sounds like this.
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
The decorator supports the Open-Closed Principle by its nature. What a great pattern.
Logging Decorator
You are going to create a LoggingLocationService
which is the Decorator of your Component LocationService
.
public class LoggingLocationService : ILocationService
{
private ILocationService _innerLocationService;
private ILogger<LoggingLocationService> _logger;
public LoggingLocationService(ILocationService locationService, ILogger<LoggingLocationService> logger)
{
_innerLocationService = locationService;
_logger = logger;
}
public List<LocationDto> GetLocations()
{
Stopwatch sw = Stopwatch.StartNew();
List<LocationDto> locations = innerLocationService.GetLocations();
sw.Stop();
long elapsedMillis = sw.ElapsedMilliseconds;
_logger.LogWarning($"Retrieved location data - Elapsed ms{elapsedMillis}");
return locations;
}
}
Nice. I noticed that you are using a built-in Stopwatch class to measure elapsed time in milliseconds. It’s much better than my solution to deducting two variables with different DateTime.Now
values.
Now you need to do a little change to the calling code, which you can find in the Index.cshtml.cs class. LocationService
initialization looks like this.
public IndexModel(IConfiguration configuration)
{
string apiKey = configuration.GetValue<String>("AppSettings:CompanyCarLocationsApiKey");
_locationService = new LocationService(apiKey);
}
You need to replace the new LocationService(apiKey)
with:
new LoggingLocationService(
new LocationService(apiKey),
_loggerFactory.CreateLogger<LocationService>())
and that’s it. You are ready to go.
Story continues
Thankfully of implemented logging, you were able to find a cause of long loading time. You did some magic as developers do and the bug no longer exists.
While you were coding, a wild idea came into your mind; “Maybe I can cache the collection of locations into memory and short loading time even more”.
CachingLocationService
After a conversation with teammates, you decide to cache data for one hour. It sounds like a reasonable amount of time. Time for another Decorator.
public class CachingLocationService : ILocationService
{
private IMemoryCache _cache;
private ILocationService _innerLocationService;
public CachingLocationService(
ILocationService locationService, IMemoryCache cache)
{
_innerLocationService = locationService;
_cache = cache;
}
public List<LocationDto> GetLocations()
{
string cacheKey = $"Locations::faef89DWWD890";
if ( _cache.TryGetValue(cacheKey, out List<LocationDto> locations))
{
return locations;
}
else
{
locations = _innerLocationService.GetLocations();
_cache.Set<List<LocationDto>>
(cacheKey, locations, TimeSpan.FromMinutes(60));
return locations;
}
}
}
Looking good. I see you are using a built-in MemoryCache class. Also, there is no need to generate a random cache key because you are loading a whole collection, right?
In the first if
statement, you simply try to get the value of locations collection by cache key. If the collection is empty, new data will be loaded. Therefore, you set a new record into the cache with the expiration of the hour.
Now I guess you would like to combine both new decorators. Logging Decorator and Caching Decorator together.
_locationService = new CachingLocationService(
new LoggingLocationService(
new LocationService(apiKey), logger), memoryCache);
You wrapped the initialization of LoggingLocationService with the initialization of CachingLocationService. It looks a little bit unreadable. Try to use multiple variables.
var withLocationService = new LocationService(apiKey);
var withLoggingLocationService = new LoggingLocationService(withLocationService, logger);
var cachingLocationService = new CachingLocationService(withLoggingLocationService, memoryCache)
Much better. When I see code where you can initialize instances with different behaviors, I am using a Builder Pattern. You can explore it, but not now.
The final class diagram after our changes looks like this.
Decorator and Dependency Injection
Before any Decorator implementations, registration into the container of LoggingService in the Sample project looked like an example below.
services.AddScoped<ILocationService>(serviceProvider =>
new LocationService(apiKey));
In this case, we are using a build-in .NET Core IoC container. This registration means that injected ILocationService
via constructor will always provide an instance of the LocationService
object. But you want an instance of CachingLocationService
.
You can take a code of multiple initializations shown at the end of the tutorial and use it in registration like this.
services.AddScoped<ILocationService>(serviceProvider =>
{
String apiKey = Configuration
.GetValue<String>("AppSettings:LocationCarsApiKey");var logger = serviceProvider
.GetService<ILogger<LoggingLocationService>>();var memoryCache = serviceProvider.GetService<IMemoryCache>();ILocationService locationService = new LocationService(apiKey);
ILocationService withLoggingLocationService =
new LoggingLocationService(locationService, logger);
ILocationService withCachingLocationService =
new CachingLocationService(withLoggingLocationService,
memoryCache);
return withCachingLocationService;
});
There is a third option. You can use a great NuGet package named Scrutor. It is an extension of dependency injection in .NET. It adds methods for registration to make registrations more fluently readable. Cool, right?
services.AddScoped<ILocationService>
(serviceProvider => new LocationService(apiKey));services.Decorate<ILocationService>((inner, provider) =>
new LoggingLocationService(inner,
provider.GetService<ILogger<LoggingLocationService>>()));services.Decorate<ILocationService>((inner, provider) =>
new CachingLocationService(inner,
provider.GetService<IMemoryCache>()));
Decorator in .NET
You can find the Decorator Pattern also in the guts of .NET. A prime example is a Stream class.
At the top of the hierarchy, we have the Stream class. This is an abstract class and requires you to provide implementations for several methods, mainly the Read and Write method. Sounds familiar, right?
Then we have several concrete implementations of the Stream class, including FileStream
, MemoryStream
, and NetworkStream
. Each of these classes would be equivalent to the LocationService
class.
Finally, we have several classes that act as decorators. These include the BufferedStream
, GZipStream
, and CryptoStream
classes.
How to recognize when to use a Decorator?
Cross-cutting concerns. These are things like logging, performance tracking, caching, and authorization. Whenever you have these cross-cutting concerns come up, you can implement them using a Decorator Pattern.
Another typical situation is the modification of data before being passed into the Component. For example, encrypt and decrypt data or conversion of units.
Build-in shortcut in Visual Studio
There is a built‑in refactoring tool in Visual Studio, either with the keyboard shortcut of Ctrl+R and then Ctrl+I. Visual Studio will create a Decorator class from the selected interface.
Summary
Remember, patterns are just guiding rails, not prescriptive implementation rules. They are not set into stone. Decorator Pattern is dynamic, clean, beloved principles supportive, and you should consider it as a new tool to your toolbox. The sample project can be found at Github.