We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Exploring ASP.NET Core Fundamentals - Understanding the Decorator Pattern

Design Patterns  • Posted 8 months ago

In a previous post we have discussed what a Repository pattern is, and how it can be useful in bringing out a Separation of concerns (S) in the application logic. In this article, let's discuss about another simple structural pattern which can help in easy extension of an existing class / component. We call it the decorator pattern which can be defined as follows:

"the decorator pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class."

In simpler terms, let's assume an already implemented class C which is responsible for a certain functionality. Now we require to add a new functionality to the existing class C, which is being used by several dependencies. We can't simply modify the component since it can lead to side effects or can impact the other dependencies which use the class C. In such cases, we use the decorator pattern which acts as an extension to the existing class without disturbing the original implementation.

Let's assume a class DataReader which is responsible for reading data from a given source, which has already been implemented in our application. Now we are required to add an additional functionality which formats the received data from the data source for a certain component. We can't simply modify the existing reader class which has already been implemented and being used in the application, which is a bad practice. For this we can apply the decorator pattern as follows:

DataReader.cs:

public class DataReader 
{
	public List<Data> ReadData() 
	{
		var data = new List<Data>();
		// some logic for reading data 
		data.Add(...);
		return data;
	}
}

Now we extend the above implementation of the DataReader by extracting an interface and adding a new implementation which internally invokes the above class (reusing the concern).

IDataReader.cs:

public interface IDataReader
{
	List<Data> ReadData();	
} 

public class DataReader : IDataReader
{
	public List<Data> ReadData() 
	{
		var data = new List<Data>();
		// some logic for reading data 
		data.Add(...);
		return data;
	}
}

The above change is not going to cause any impact, coz we have extracted an interface from the existing implementation (which isn't a modification).

Now we create a new implementation for the IDataReader called the FormattedDataReader which contains the new implementation we are required to do on the actual DataReader class.

public class FormattedDataReader : IDataReader
{
	IDataReader reader;

	public FormattedDataReader(IDataReader reader) 
	{
		this.reader = reader;
	}

	public List<Data> ReadData()
	{
		List<Data> data = this.reader.ReadData();
		//additional logic on the data (the new requirement)
		return data;		
	}
}

Notice that we have added a parameterized constructor for the new class FormattedDataReader which accepts an object of type IDataReader and is assigned to a class variable of the same name. Also notice the implementation we have for the method ReadData() defined in the IDataReader interface. The class invokes the same method from the reference passed through the constructor. The data (assigned to the variable data) contains the base data on top of which we add our new logic and return the same.

now the Consumer class shall be:

Consumer.cs:

public class Consumer 
{
	IDataReader reader;

	public Consumer()
	{
		// a new object of the type FormattedDataReader is created 
    // and in the parameter we pass the actual DataReader object 
    // which is the original implementation
		reader = new FormattedDataReader(new DataReader());		
	}

	public void HandleFormattedDataFromReader() 
	{
		// the ReadData() method invokes the same 
    // from FormattedDataReader class
    // (for which the instantiation has been done)
		List<Data> data = reader.ReadData();
		... 
		// The consumer logic goes in here
	}
}

In this way we can extend any implemented class without the need for having to modify it.

This pattern is used to follow the principle Open/Closed principle (O) in the SOLID principles which states that

"any software entity (classes, modules, functions etc.) should be open for extension and closed for modification".

In the above example, if used under a DI container (like in the ASP.NET Core, or containers like Ninject) we register the type FormattedDataReader as an injectable with the constructor parameter being passed an object of DataReader.