๐Ÿ“„

Decorator Pattern

Intermediate 1 min read 100 words

Attach additional responsibilities to objects dynamically

Decorator Pattern

Intent

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Problem

You want to add behavior to objects without:

  • Modifying their code
  • Creating a complex inheritance hierarchy
  • Affecting other objects of the same class

Classic Implementation

// Component interface
public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

// Concrete component
public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple coffee";
    public decimal GetCost() => 2.00m;
}

// Decorator base class
public abstract class CoffeeDecorator : ICoffee
{
    protected readonly ICoffee _coffee;

    protected CoffeeDecorator(ICoffee coffee)
    {
        _coffee = coffee;
    }

    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual decimal GetCost() => _coffee.GetCost();
}

// Concrete decorators
public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{_coffee.GetDescription()}, milk";
    public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}

public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{_coffee.GetDescription()}, sugar";
    public override decimal GetCost() => _coffee.GetCost() + 0.25m;
}

public class WhippedCreamDecorator : CoffeeDecorator
{
    public WhippedCreamDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{_coffee.GetDescription()}, whipped cream";
    public override decimal GetCost() => _coffee.GetCost() + 0.75m;
}

public class CaramelDecorator : CoffeeDecorator
{
    public CaramelDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{_coffee.GetDescription()}, caramel";
    public override decimal GetCost() => _coffee.GetCost() + 0.60m;
}

// Usage - Stack decorators
ICoffee coffee = new SimpleCoffee();
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee: $2.00

coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk: $2.50

coffee = new SugarDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk, sugar: $2.75

coffee = new WhippedCreamDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk, sugar, whipped cream: $3.50

// One-liner creation
ICoffee fancy = new CaramelDecorator(
                    new WhippedCreamDecorator(
                        new MilkDecorator(
                            new SimpleCoffee())));

Real-World Example: Repository Decorators

// Repository interface
public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}

// Base implementation
public class Repository<T> : IRepository<T> where T : class
{
    protected readonly DbContext _context;

    public Repository(DbContext context)
    {
        _context = context;
    }

    public virtual async Task<T> GetByIdAsync(int id)
        => await _context.Set<T>().FindAsync(id);

    public virtual async Task<List<T>> GetAllAsync()
        => await _context.Set<T>().ToListAsync();

    public virtual async Task AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
    }

    public virtual async Task UpdateAsync(T entity)
    {
        _context.Set<T>().Update(entity);
        await _context.SaveChangesAsync();
    }

    public virtual async Task DeleteAsync(int id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null)
        {
            _context.Set<T>().Remove(entity);
            await _context.SaveChangesAsync();
        }
    }
}

// Caching decorator
public class CachedRepository<T> : IRepository<T> where T : class
{
    private readonly IRepository<T> _inner;
    private readonly IDistributedCache _cache;
    private readonly TimeSpan _expiration;

    public CachedRepository(
        IRepository<T> inner,
        IDistributedCache cache,
        TimeSpan? expiration = null)
    {
        _inner = inner;
        _cache = cache;
        _expiration = expiration ?? TimeSpan.FromMinutes(5);
    }

    public async Task<T> GetByIdAsync(int id)
    {
        var cacheKey = $"{typeof(T).Name}:{id}";
        var cached = await _cache.GetStringAsync(cacheKey);

        if (!string.IsNullOrEmpty(cached))
            return JsonSerializer.Deserialize<T>(cached);

        var entity = await _inner.GetByIdAsync(id);

        if (entity != null)
        {
            await _cache.SetStringAsync(cacheKey,
                JsonSerializer.Serialize(entity),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = _expiration
                });
        }

        return entity;
    }

    public async Task<List<T>> GetAllAsync()
    {
        // Could cache this too with appropriate key
        return await _inner.GetAllAsync();
    }

    public async Task AddAsync(T entity)
    {
        await _inner.AddAsync(entity);
        // No cache invalidation needed for add
    }

    public async Task UpdateAsync(T entity)
    {
        await _inner.UpdateAsync(entity);
        // Invalidate cache - implementation depends on how to get ID
    }

    public async Task DeleteAsync(int id)
    {
        await _inner.DeleteAsync(id);
        var cacheKey = $"{typeof(T).Name}:{id}";
        await _cache.RemoveAsync(cacheKey);
    }
}

// Logging decorator
public class LoggingRepository<T> : IRepository<T> where T : class
{
    private readonly IRepository<T> _inner;
    private readonly ILogger<LoggingRepository<T>> _logger;

    public LoggingRepository(
        IRepository<T> inner,
        ILogger<LoggingRepository<T>> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<T> GetByIdAsync(int id)
    {
        _logger.LogDebug("Getting {Type} with ID {Id}", typeof(T).Name, id);
        var sw = Stopwatch.StartNew();

        var result = await _inner.GetByIdAsync(id);

        sw.Stop();
        _logger.LogDebug("Retrieved {Type} in {Elapsed}ms",
            typeof(T).Name, sw.ElapsedMilliseconds);

        return result;
    }

    public async Task<List<T>> GetAllAsync()
    {
        _logger.LogDebug("Getting all {Type}", typeof(T).Name);
        var sw = Stopwatch.StartNew();

        var result = await _inner.GetAllAsync();

        sw.Stop();
        _logger.LogDebug("Retrieved {Count} {Type} in {Elapsed}ms",
            result.Count, typeof(T).Name, sw.ElapsedMilliseconds);

        return result;
    }

    public async Task AddAsync(T entity)
    {
        _logger.LogInformation("Adding new {Type}", typeof(T).Name);
        await _inner.AddAsync(entity);
        _logger.LogInformation("Added {Type}", typeof(T).Name);
    }

    public async Task UpdateAsync(T entity)
    {
        _logger.LogInformation("Updating {Type}", typeof(T).Name);
        await _inner.UpdateAsync(entity);
        _logger.LogInformation("Updated {Type}", typeof(T).Name);
    }

    public async Task DeleteAsync(int id)
    {
        _logger.LogInformation("Deleting {Type} with ID {Id}", typeof(T).Name, id);
        await _inner.DeleteAsync(id);
        _logger.LogInformation("Deleted {Type} with ID {Id}", typeof(T).Name, id);
    }
}

// Retry decorator
public class RetryRepository<T> : IRepository<T> where T : class
{
    private readonly IRepository<T> _inner;
    private readonly int _maxRetries;
    private readonly TimeSpan _delay;

    public RetryRepository(IRepository<T> inner, int maxRetries = 3)
    {
        _inner = inner;
        _maxRetries = maxRetries;
        _delay = TimeSpan.FromSeconds(1);
    }

    public async Task<T> GetByIdAsync(int id)
        => await ExecuteWithRetryAsync(() => _inner.GetByIdAsync(id));

    public async Task<List<T>> GetAllAsync()
        => await ExecuteWithRetryAsync(() => _inner.GetAllAsync());

    public async Task AddAsync(T entity)
        => await ExecuteWithRetryAsync(async () =>
        {
            await _inner.AddAsync(entity);
            return true;
        });

    public async Task UpdateAsync(T entity)
        => await ExecuteWithRetryAsync(async () =>
        {
            await _inner.UpdateAsync(entity);
            return true;
        });

    public async Task DeleteAsync(int id)
        => await ExecuteWithRetryAsync(async () =>
        {
            await _inner.DeleteAsync(id);
            return true;
        });

    private async Task<TResult> ExecuteWithRetryAsync<TResult>(Func<Task<TResult>> action)
    {
        var attempts = 0;
        while (true)
        {
            try
            {
                return await action();
            }
            catch (Exception) when (++attempts < _maxRetries)
            {
                await Task.Delay(_delay * attempts);
            }
        }
    }
}

// Compose decorators
IRepository<Product> repository = new Repository<Product>(context);
repository = new LoggingRepository<Product>(repository, logger);
repository = new CachedRepository<Product>(repository, cache);
repository = new RetryRepository<Product>(repository);

// Now every operation is: Retry โ†’ Cache โ†’ Log โ†’ Database

DI Registration for Decorators

// Manual decoration
services.AddScoped<Repository<Product>>();
services.AddScoped<IRepository<Product>>(sp =>
{
    var context = sp.GetRequiredService<DbContext>();
    var cache = sp.GetRequiredService<IDistributedCache>();
    var logger = sp.GetRequiredService<ILogger<LoggingRepository<Product>>>();

    IRepository<Product> repo = new Repository<Product>(context);
    repo = new LoggingRepository<Product>(repo, logger);
    repo = new CachedRepository<Product>(repo, cache);
    return repo;
});

// Or use a library like Scrutor
services.AddScoped<IRepository<Product>, Repository<Product>>();
services.Decorate<IRepository<Product>, LoggingRepository<Product>>();
services.Decorate<IRepository<Product>, CachedRepository<Product>>();

.NET Stream Decorators

.NET uses Decorator extensively with streams:

// Base stream
using var fileStream = File.OpenRead("data.txt");

// Add buffering decorator
using var bufferedStream = new BufferedStream(fileStream);

// Add compression decorator
using var gzipStream = new GZipStream(bufferedStream, CompressionMode.Compress);

// Add encryption decorator
using var cryptoStream = new CryptoStream(gzipStream, encryptor, CryptoStreamMode.Write);

// Write through all decorators
await cryptoStream.WriteAsync(data);
// Data flows: CryptoStream โ†’ GZipStream โ†’ BufferedStream โ†’ FileStream

Decorator vs Inheritance

// WITH Inheritance - Combinatorial explosion
public class Coffee { }
public class CoffeeWithMilk : Coffee { }
public class CoffeeWithSugar : Coffee { }
public class CoffeeWithMilkAndSugar : Coffee { }
public class CoffeeWithWhippedCream : Coffee { }
public class CoffeeWithMilkAndWhippedCream : Coffee { }
// ... many more combinations

// WITH Decorator - Compose at runtime
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);     // Add milk
coffee = new SugarDecorator(coffee);    // Add sugar
// Any combination possible without new classes

Interview Tips

Common Questions:

  • โ€œExplain Decorator pattern with an exampleโ€
  • โ€œHow is Decorator different from inheritance?โ€
  • โ€œWhere have you seen Decorator in .NET?โ€

Key Points:

  1. Decorator wraps objects to add behavior
  2. Implements same interface as wrapped object
  3. Decorators can be stacked in any order
  4. More flexible than inheritance
  5. Great for cross-cutting concerns (logging, caching, retry)

Decorator vs Proxy:

  • Decorator: Adds functionality
  • Proxy: Controls access (lazy loading, security)

.NET Examples:

  • Stream wrappers (BufferedStream, GZipStream, CryptoStream)
  • HttpClientHandler decorators
  • ASP.NET Core middleware (similar concept)