
Structural Design Pattern - Decorator
Author - Abdul Rahman (Bhai)
Design Pattern
13 Articles
Table of Contents
What we gonna do?
In this article, let's learn about Decorator Design Pattern in .NET.
The decorator pattern is a design pattern that allow dynamically attach additional responsibilities to an object, providing a flexible alternative to subclassing for extending functionality. Also referred to as a wrapper, the decorator pattern allows for the dynamic addition of responsibilities to an object at runtime.
By using the decorator pattern, we can avoid violating Single Responsibility Principle. Our classes would get littered with code that doesn't necessarily belong there. That's where the decorator pattern comes in.
Why we gonna do?
Without the Decorator pattern, extending an object's behavior typically requires subclassing. This quickly leads to a combinatorial explosion of classes when you need different combinations of features — for example, a cached repository, a logged repository, and a cached-and-logged repository would all need separate subclasses.
Subclassing is also static — you cannot change an object's behavior at runtime. Any adjustment requires modifying or adding new classes, which is costly and rigid.
The Decorator pattern solves this by wrapping objects in decorator classes that add behavior transparently. Decorators can be stacked and combined at runtime, keeping each class focused on a single concern and avoiding bloated inheritance hierarchies.
How we gonna do?
Structure
- Component (IRepository):
- Defines the interface for objects that can have responsibilities added to them.
- Concrete Component (DatabaseRepository):
- Defines the logic to read data from database.
- Decorator (RepositoryDecoratorBase):
- An abstract class implementing the same interface as the component and maintains a reference to a component object. It defines an interface that conforms to the component's interface.
- Concrete Decorator (CachedRepositoryDecorator):
- Implements the decorator abstract class and adds specific behavior or state to the component.
// Component
public interface IRepository
{
string ReadData();
}
// Concrete Component
public class DatabaseRepository : IRepository
{
public string ReadData()
{
return "Data From Database";
}
}
// Decorator
public abstract class RepositoryDecoratorBase(IRepository repository) : IRepository
{
public virtual string ReadData()
{
return repository.ReadData();
}
}
// Concrete Decorator
public class CachedDatabaseRepository(IRepository repository) : RepositoryDecoratorBase(repository)
{
public override string ReadData()
{
Random gen = new();
int prob = gen.Next(100);
if (prob < 20)
{
return "Data from Cache";
}
return base.ReadData();
}
}
In the above code, we're implementing the decorator pattern for a repository system. The IRepository interface defines the base functionality with a ReadData method. The DatabaseRepository class is a concrete component that retrieves data from a database. The RepositoryDecoratorBase abstract class acts as the decorator base, maintaining a reference to a repository and delegating the ReadData method. Finally, the CachedDatabaseRepository class is a concrete decorator that extends the behavior by adding caching logic. If a random probability is less than 20%, it returns cached data; otherwise, it delegates to the base repository's ReadData method. This structure allows dynamic composition of repository behaviors, adding features like caching without modifying the core repository code.
Use Cases
- Dynamic Responsibility Addition - When there's a need to add responsibilities to individual objects dynamically at runtime without affecting other objects.
- Dynamic Responsibility Removal - When the added responsibilities need to be withdrawn dynamically.
- Extension by Subclass is Impractical - When extending functionality through subclassing results in an impractical or impossible solution.
Advantages
- Flexibility - Dynamically add or remove responsibilities at runtime.
- Single Responsibility Principle - Helps adhere to the single responsibility principle by separating concerns.
Disadvantages
System Litter - This pattern may result in a system with many small, simple classes, potentially increasing the effort required for learning and debugging. Despite these points, the Decorator pattern is a useful starting point because of its simplicity. Finally, let's have a look at related patterns.
Related Patterns
- adapter pattern
- composite pattern
- strategy pattern
Summary
In this article we learn't about the decorator pattern that provides a flexible way to extend the functionality of objects dynamically at runtime. It promotes flexibility and adherence to the single responsibility principle, although it may lead to a system with many small classes. Understanding its structure and use cases enables developers to leverage its benefits effectively.