Design Patterns - Decorator

Decorator Pattern

A structural design pattern used for dynamically adding behavior to a class without making changes to that class.

The Decorator Class will take an object implementing the same interface. This allows us to pass the object being decorated into the decorator object and allows the decorator object to act as a wrapper
around this original object.

The Decorator object will keep a reference of the object being decorated(the component object). Because the decorator object implement the same interface as the original component object, it now has a chance to intercept any method calls on the interface and inject some additional behavior into those calls.

Decorator Class and be nested.

This is the Example we are going to use.

Using Decorator Objects

1
2
3
4
5
6
7
8
// Standard component instantiation
IWeatherService weatherService = new WeatherService();

// Instantiation with decorator objects
IWeatherService weatherService =
new CachingDecorator(
new LogginDecorator(
new WeatherService()));

To achieve this, we need to make sure the original component class and all the decorator classes need to implement from the same interface. And all decortor classes need to take the object of type IWeatherService in their constructors.

Logging Decorator

Log how often a method was called, how long it took, parameters and responses.

1
2
3
4
5
6
public interface IWeatherService
{
CurrentWeather GetCurrentWeather(String location);

LocationForecast GetForecast(String location);
}

This is the interface for our original WeatherService Class and our new Decorator Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class WeatherServiceLoggingDecorator : IWeatherService
{
private IWeatherService _weatherService;
private ILogger<WeatherServiceLoggingDecorator> _logger;

public WeatherServiceLoggingDecorator(IWeatherService weatherService, ILogger<WeatherServiceLoggingDecorator> logger)
{
_weatherService = weatherService;
_logger = logger;
}
public CurrentWeather GetCurrentWeather(string location)
{
Stopwatch sw = Stopwatch.StartNew();
CurrentWeather currentWeather = _weatherService.GetCurrentWeather(location);
sw.Stop();
long elapsedMillis = sw.ElapsedMilliseconds;

_logger.LogWarning("Retrieved weather data for {location} - Elapsed ms: {} {@currentWeather}", location, elapsedMillis, currentWeather);

return currentWeather;
}

public LocationForecast GetForecast(string location)
{
Stopwatch sw = Stopwatch.StartNew();
LocationForecast locationForecast = _weatherService.GetForecast(location);
sw.Stop();
long elapsedMillis = sw.ElapsedMilliseconds;

_logger.LogWarning("Retrieved weather data for {location} - Elapsed ms: {} {@locationForecast}", location, elapsedMillis, locationForecast);

return locationForecast;
}
}

This is our new Decorator Class, we implement from the IWeatherService Interface, it is taking the interface as a parameter in the constructor, and implemented two methods. In the GetCurrentWeather() method, it logs the time it takes to run the method, then calling the original _weatherService.GetCurrentWeather() method.

Caching Decorator

Cache weather conditions, forecasts for a city to reduce the number of external API calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class WeatherServiceCachingDecorator : IWeatherService
{
private IWeatherService _weatherService;
private IMemoryCache _cache;

public WeatherServiceCachingDecorator(IWeatherService weatherService, IMemoryCache cache)
{
_weatherService = weatherService;
_cache = cache;
}

public CurrentWeather GetCurrentWeather(string location)
{
// if we can found value in the cache, return it
// otherwise get the current weather then add it to the cache for 30 mins
string cacheKey = $"WeatherConditions::{location}";
if (_cache.TryGetValue<CurrentWeather>(cacheKey, out var currentWeather))
{
return currentWeather;
}
else
{
var currentConditions = _weatherService.GetCurrentWeather(location);
_cache.Set<CurrentWeather>(cacheKey, currentConditions, TimeSpan.FromMinutes(30));
return currentConditions;
}
}

public LocationForecast GetForecast(string location)
{
string cacheKey = $"WeatherForecast::{location}";
if (_cache.TryGetValue<LocationForecast>(cacheKey, out var forecast))
{
return forecast;
}
else
{
var locationForecast = _weatherService.GetForecast(location);
_cache.Set<LocationForecast>(cacheKey, locationForecast, TimeSpan.FromMinutes(30));
return locationForecast;
}
}
}

And meanwhile in the HomeController, we need to build this onion like structure from inside to outside.

1
2
3
4
5
IWeatherService weatherService = new WeatherService(apiKey);
IWeatherService withLoggingDecorator = new WeatherServiceLoggingDecorator(weatherService, _loggerFactory.CreateLogger<WeatherServiceLoggingDecorator>());
IWeatherService withCachingDecorator = new WeatherServiceCachingDecorator(withLoggingDecorator, memoryCache);

_weatherService = withCachingDecorator;

The call stack will be: CachingDecorator => LoggingDecorator => WeatherService

Decorator Summary

  • Multiple decorators can be used in conjunction with one another
  • Each decorator can focus on a single task, promoting separation of concerns
  • Decorator classes allow functionality to be added dynamically

Decorator Pattern Characteristics

  1. Implement the same base interface as the original object
  2. Take a instance of the original object as part of their constructor
  3. Add new behaviors to the original object they are wrapping

Using Decorators with Dependency Injection Container

.NET Core has built in IoC container which will help us to create WeatherService object when we need it and manage the lifetime of object.

We could simplify the HomeController constructor to this

1
2
3
4
5
6
private readonly IWeatherService _weatherService;

public HomeController(ILogger<HomeController> logger, IWeatherService weatherService)
{
_weatherService = weatherService;
}

And in the startUp.cs, we configure the IoC container to this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

services.AddMemoryCache();

String apiKey = Configuration.GetValue<String>("OpenWeatherMapApiKey");
services.AddScoped<IWeatherService>(serviceProvider =>
{
String apiKey = Configuration.GetValue<String>("OpenWeatherMapApiKey");

var logger = serviceProvider.GetService<ILogger<WeatherServiceLoggingDecorator>>();

var memoryCache = serviceProvider.GetService<IMemoryCache>();

IWeatherService weatherService = new WeatherService(apiKey);
IWeatherService withLoggingDecorator = new WeatherServiceLoggingDecorator(weatherService, logger);
IWeatherService withCachingDecorator = new WeatherServiceCachingDecorator(withLoggingDecorator, memoryCache);

return withCachingDecorator;
});
}

Now whenever we need a IWeatherService object, it will be created and provided to us with this structure. (CachingDecorator => LoggingDecorator => WeatherService)

When to use Decorator Pattern

  • Cross cutting concerns
    • Logging, Performance Tracking(Timer, StopWatch…), Caching, Authorization
  • Manipulate data going to/from component
    • object we need to encrypt and decrypt before being passed to a component

Question: What if your component does not have an interface/extend from a base class?

  • Extract an interface from the class

What if you can’t modify the class?

  • Adapter Pattern

To put a class in front of your component and extract an interface from the Adapter Class

Summary

  • Design Patterns are about ideas
  • Interfaces allow us to create loosely coupled designs
  • the decorator pattern adds the ability to dynamically add behavior
  • This is accomplished by wrapping around the original object and intercepting methods