The code is available on GitHub here
The Decorator Pattern adds new responsibility to existing objects dynamically. It also uses composition over inheritance to extend behavior. Many decorators can be chained together creating more complex scenarios.
When to use
The Decorator Pattern can be a good fit if you want to extend the functionality of an object at runtime. Extending functionality with composition leads to less coupled designs in comparison with inheritance, which is also determined at compile time and cannot be easily changed.
Also, another powerful characteristic, is that it is possible to add multiple new responsibilities to objects by writing new code, rather than altering existing code. Hence, you can apply an arbitrary number of decorators to a component satisfying more complex scenarios. That means, you can reuse old decorators with new ones in any combination, instead of altering existing code, to build the desired behavior.
Decorator, a deep dive
Diving into the mechanics of the pattern, we will explore its key components gaining the knowledge of how we can implement it.
A central component of the Decorator Pattern is the ComponentDecorator. It is an abstract class (or interface) that extends the type of the component which is going to be decorated. This inheritance has nothing to do with reusability of behavior, it only exists for type compatibility between decorators and the component to be decorated. As a result, it enables chaining many decorator one inside the other. Also, the decorator class has an instance variable (this part is related with the composition) which is a reference to the decorated component or to another decorator.
Given that each decorator has the same type as the component it decorates, we can pass around a decorated object in place of the original. That way, we can build a complex object using many decorators, each of them wraps the others.
Finally, each concrete decorator extends from the ComponentDecorator class and overrides the needed methods. The new functionality is added before and/or after delegating to the object it decorates. This is a flexible alternative to subclassing for extending functionality.
Below we present the Decorator Pattern class diagram.
The key in the previous design is the composition arrow from the ComponentDecorator to the IComponent interface of the object that we decorate. That means, that every concrete decorator will have an instance variable of type IComponent which also enables the decorators to be nested together.
Build a Caching mechanism
Now that we have a solid knowledge about what the Decorator Pattern is and what it does, we are going to gain knowledge about how we can implement it. As an example, we will implement a caching mechanism that stores key-value pairs to disk. This mechanism is represented by an object named FileCache that plays the role of the component that will be decorated with additional functionality.
The FileCache class implements the ICache interface (shown below) that has a Get method for retrieving a value given a unique key, a Set method that sets a key and its value and an Exists method which checks if the requested key exists.
public interface ICache
{
string Get(string key);
void Set(string key, string value);
bool Exists(string key);
}
Below, the implementation of the FileCache, which is a concrete implementation of the ICache
public class FileCache : ICache
{
private readonly string _location;
public FileCache(string location)
{
Directory.CreateDirectory(location);
_location = location;
}
public string GetFilePath(string key)
=> Path.Combine(_location, key);
public bool Exists(string key)
=> File.Exists(GetFilePath(key));
public string Get(string key)
{
if (!Exists(key))
throw new KeyNotFoundException(key);
return File.ReadAllText(GetFilePath(key));
}
public void Set(string key, string value)
{
if (Exists(key))
File.Delete(GetFilePath(key));
File.WriteAllText(GetFilePath(key), value);
}
}
The implementation of the Cache simply writes and retrieves data from the disc.
Enhance Cache functionality with hash security
Our cache is ready and can be used without any other modification. However, suppose a new security policy needs to be implemented for the existing cache. For this policy we should implement a hash validation of the cached data we retrieve. Before we begin coding, we explore some of our options:
- We can modify the existing Cache implementation to add the security mechanism.
- We can create a different class with its own interface that handles the security aspect and has a reference to the cache object.
- We can create a new class that is compatible with the ICache interface that implements the security mechanism and has a reference to the cache object.
In this example, we will take the 3rd option. One difference with the 2nd is that the interface will remain the same. That means the rest of the application would not need to be changed so the security aspect will be handled transparently. But before we move forward implementing our first concrete decorator (which will sit between the ICache interface and the FileCache) lets explore one design principle first and see how it fits with our case.
Open Close Principle
Implementing the Decorator Pattern would also result for our design to respect the Open Close Principle which states the following:
Classes should be open for extension, but closed for modification
In other words, our software should allow classes to be easily extended in order to incorporate new behavior without modifying existing code. Those designs are more resilient to change and flexible enough to meet changing requirements.
Implementing the ComponentDecorator for the ICache
The next step is to create the abstract CacheDecorator class which plays the role of the ComponentDecorator. This class will extend the ICache interface and all future decorators will be subclasses of it.
public abstract class CacheDecorator : ICache
{
protected readonly ICache _inner;
protected CacheDecorator(ICache inner)
{
_inner = inner;
}
public virtual bool Exists(string key)
=> _inner.Exists(key);
public virtual string Get(string key)
=> _inner.Get(key);
public virtual void Set(string key, string value)
=> _inner.Set(key, value);
}
There is no rule that dictates if the ComponentDecorator class will be an abstract class or just an interface. In our case, we choose an abstract class that have also a default implementation of all overridden methods of the ICache interface.
The important information the CacheDecorator holds, is the reference to an instance of ICache. That instance could be a FileCache or could be another decorator.
Build a concrete decorator to apply hashing functionality
The hashing functionality will ensure the data we get from the cache are not tampered. This can be achieved creating a new decorator, named SecuredCache, which extends from CacheDecorator.
Inside the SecuredCache implementation, we override its Get method and immediately forward the Get call to the inner ICache instance. Afterwards, we compute the hash of the value that will be returned and compare it with the one that was stored during the Set method. Finally, we if the two hashes are equal we return the data, otherwise we throw an exception.
Implementing the Set method, we forward the Set call to the inner cache instance and then we compute the hash of the stored value. The computed hash will also be stored to disk in a key-value manner (The key is the file name and the value is the file’s content).
public class SecuredCache : CacheDecorator
{
private readonly string _location;
public SecuredCache(string location, ICache inner) : base(inner)
{
Directory.CreateDirectory(location);
this._location = location;
}
public override void Set(string key, string value)
{
_inner.Set(key, value);
var hash = CalculateHash(value);
StoreHash(key, hash);
}
public override string Get(string key)
{
var entry = _inner.Get(key);
var hash = CalculateHash(entry);
EnsureHashIsValid(key, hash);
return entry;
}
private byte[] CalculateHash(string entry)
{
var tmpSource = ASCIIEncoding.ASCII.GetBytes(entry);
var tmpHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource);
return tmpHash;
}
private void StoreHash(string key, byte[] data)
=> File.WriteAllBytes(Path.Combine(_location, key), data);
private void EnsureHashIsValid(string key, byte[] hash)
{
var oldHash = File.ReadAllBytes(Path.Combine(_location, key));
if (!CompareByteArrays(oldHash, hash))
throw new HashValidationException($"Hashes are not equal! Key {key} is not valid");
}
}
The CompareByteArrays is not displayed here in order to simplify the code. However, you can find the full code on https://github.com/dkokkinos/design-patterns/tree/master/Decorator.
SecuredCache usage example
Now its time to show some examples of how we can use the SecuredCache decorator in combination with our FileCache, but first, we present a client that will use our final ICache object shown below:
public class Client
{
private readonly ICache _cache;
public Client(ICache cache)
{
_cache = cache;
}
public void DoWork()
{
_cache.Set("a", "11111");
_cache.Set("b", "22222");
_cache.Set("c", "33333");
_cache.Set("b", "23456");
var a = _cache.Get("a");
var b = _cache.Get("b");
var c = _cache.Get("c");
Console.WriteLine("The entry with key a has value: " + a);
Console.WriteLine("The entry with key b has value: " + b);
Console.WriteLine("The entry with key c has value: " + c);
}
public void DoAnotherWork()
{
var a = _cache.Get("a");
Console.WriteLine("The entry with key a has value: " + a);
}
}
The Client has a reference to an ICache object. This could be a FileCache or a SecuredCache containing the FileCache. That information is irrelevant to the Client. The Client also has two methods that do very basic use of the cache and will be used in our examples.
Next, we build two scenarios. In the first one, the Client uses the FileCache implementation and in the second the SecuredCache.
public static void Main()
{
Console.WriteLine("---------------- SimpleCacheScenario ----------------");
SimpleCacheScenario();
Console.WriteLine();
Console.WriteLine("---------------- SecuredCacheScenario ----------------");
SecuredCacheScenario();
Console.WriteLine();
}
Additionally, we have the implementations of those two scenarios. The interesting part is in SecuredCacheScenario where we deliberately tamper the data by changing the contents of one of its files. This causes a mismatch between the hash of that content and the new content and a HashValidationException should be raised.
private static void SimpleCacheScenario()
{
var cache = new FileCache(Path.Combine(nameof(SimpleCacheScenario), "cache"));
var client = new Client(cache);
client.DoWork();
}
private static void SecuredCacheScenario()
{
var cache = new FileCache(Path.Combine(nameof(SecuredCacheScenario), "cache"));
var securedCache = new SecuredCache(Path.Combine(nameof(SecuredCacheScenario), "hashes"), cache);
var client = new Client(securedCache);
client.DoWork();
// We deliberately change the content of the cache! This should cause a HashValidationException when later we Get the contents with key "a".
File.WriteAllText(Path.Combine(Path.Combine(nameof(SecuredCacheScenario), "cache"), "a"), "1112");
try
{
client.DoAnotherWork();
}
catch (HashValidationException e)
{
Console.WriteLine(e.Message);
}
}
The run of the previous code ensures us that the SecuredCache is working as expected, preventing us from getting corrupted data.
---------------- SimpleCacheScenario ----------------
The entry with key a has value: 11111
The entry with key b has value: 23456
The entry with key c has value: 33333
---------------- SecuredCacheScenario ----------------
The entry with key a has value: 11111
The entry with key b has value: 23456
The entry with key c has value: 33333
Hashes are not equal! Key a is not valid
A BufferedCache decorator
New specs have arrived that require to enhance the performance of our cache. We notice that the main time consuming process in our FileCache is writing and reading from disk. Most likely, the first thing that comes to mind, is to modify the existing cache and having perhaps a Dictionary to hold the data (acting as a buffer) and use that when a client requests data from the cache. This is not necessarily wrong, but again we fuse two responsibilities together.
Using the decorator pattern again, we can easily separate those concerns, extending our existing mechanism by introducing a new BufferedCache decorator. That component, will have the responsibility to hold the data on memory and forward the requests to the inner ICache instance only if the data are not existent in memory.
In order to implement the new decorator, we override the Get method and check if the data exists in the Dictionary, and if they do, we just return them without calling Get on the inner ICache instance (in that way, we don’t pay any other time consuming costs). Also, in Set method we forward the request to the inner ICache and update the dictionary with the new value. Below is the implementation of the BufferedCache decorator.
public class BufferedCache : CacheDecorator
{
private readonly Dictionary<string, string> values;
public BufferedCache(ICache inner) : base(inner)
{
values = new();
}
public override string Get(string key)
{
if (values.TryGetValue(key, out string value))
return value;
var entry = _inner.Get(key);
values.Add(key, entry);
return entry;
}
public override void Set(string key, string value)
{
if (values.ContainsKey(key))
values.Remove(key);
_inner.Set(key, value);
values.Add(key, value);
}
}
Having another decorator, lets explore how we can use both SecuredCache and BufferedCache together comparing the results with a run without the BufferedCache.
First, we introduce a new SecuredWithBufferedCacheScenario scenario.
public static void Main()
{
Console.WriteLine("---------------- SecuredWithBufferedCacheScenario ----------------");
SecuredWithBufferedCacheScenario();
Console.WriteLine();
}
That scenario uses the Client we already used before, but this time we add another method on the Client to simulate a lot of requests in cache in order to see a difference in time when using the BufferedCache and without.
private static void SecuredWithBufferedCacheScenario()
{
var cache = new FileCache(Path.Combine(nameof(SecuredWithBufferedCacheScenario), "cache"));
var bufferedCache = new BufferedCache(cache);
var securedAndBufferedCache = new SecuredCache(Path.Combine(nameof(SecuredWithBufferedCacheScenario), "hashes"), bufferedCache);
var client = new Client(securedAndBufferedCache);
client.DoWork();
client.DoAnotherWork();
client.DoALotOfWork();
}
The new DoALotOfWork method is implemented below:
public class Client
{
// code omitted..
public void DoALotOfWork()
{
Stopwatch sw = new Stopwatch();
sw.Start();
_cache.Set("d", "very big value...!");
for (int i = 0; i < 1000; i++)
{
var value = _cache.Get("d");
int numOfChars = value.Length; // do some calculation
}
sw.Stop();
Console.WriteLine($"The work is finished after {sw.ElapsedMilliseconds} millis");
}
}
It is a for loop that makes Get requests to the cache. Lets see how much time we save using the new BufferedCache in the run output below:
---------------- SecuredCacheScenario ----------------
The entry with key a has value: 11111
The entry with key b has value: 23456
The entry with key c has value: 33333
Hashes are not equal! Key a is not valid
The work is finished after 321 millis
---------------- SecuredWithBufferedCacheScenario ----------------
The entry with key a has value: 11111
The entry with key b has value: 23456
The entry with key c has value: 33333
The entry with key a has value: 11111
The work is finished after 129 millis
With the buffer in place the loop ends in 129 milliseconds and without in finishes after 321 milliseconds, which is a considerable difference. This result is achieved using separate classes/decorators, each one of them having its own responsibility and adding a new layer of functionality to the FileCache component.
Recommendations
If you want to learn more, refresh or better understand design patterns and software design in general, I also recommend reading the following books:
A must in Design Patterns
A very well written book with plenty of examples and insights