Skip to main content

Design Patterns: Decorator

The Decorator Pattern is a structural design pattern that allows behavior to be dynamically added to individual objects, without modifying their code. It is often used to extend the functionalities of classes in a flexible and reusable way.


When To Use It?

  • You Need to Add Behavior Dynamically at Runtime. Example: A coffee shop app where users can customize their drinks with add-ons like Milk, Sugar, Whipped Cream, etc.
  • You Want to Avoid a Large Inheritance Tree. If you use inheritance, each combination of behaviors would require a new subclass (e.g., CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, etc.).
  • You Want More Flexible and Reusable Code. Different decorators can be reused independently. Example: A LoggingDecorator, CompressionDecorator, and EncryptionDecorator can be used separately or in different orders.
  • You Follow the Open-Closed Principle. Instead of modifying an existing class, you can extend behavior using decorators. This makes your code more maintainable.
  • You Need to Combine Multiple Independent Behaviors. Example: A text editor where you can apply bold, italics, and underline without modifying the base class.

When Not To Use It?

  • The Object’s Behavior Doesn’t Change at Runtime. If an object's behavior is fixed and won’t need modifications dynamically.
  • The Number of Possible Combinations is Small.
  • Alternative Patterns Are Simpler. If you need different behaviors but don’t need stacking, the strategy pattern may be a better choice. If you have a few known configurations, a factory might be more maintainable.

Example in C#

Interface
public interface ICoffee
{
    string GetDescription();
    double GetCost();
}

Concrete Component Implementation
public class SimpleCoffee : ICoffee
{
    public string GetDescription()
    {
        return "Simple Coffee";
    }

    public double GetCost()
    {
        return 5.0; // Base price of coffee
    }
}

Abstract Decorator
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;

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

    public virtual string GetDescription()
    {
        return _coffee.GetDescription();
    }

    public virtual double GetCost()
    {
        return _coffee.GetCost();
    }
}


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

    public override string GetDescription()
    {
        return _coffee.GetDescription() + ", Milk";
    }

    public override double GetCost()
    {
        return _coffee.GetCost() + 1.5; // Extra cost for milk
    }
}

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

    public override string GetDescription()
    {
        return _coffee.GetDescription() + ", Sugar";
    }

    public override double GetCost()
    {
        return _coffee.GetCost() + 0.5; // Extra cost for sugar
    }
}



Client
class Program
{
    static void Main()
    {
        // Create a simple coffee
        ICoffee coffee = new SimpleCoffee();
        Console.WriteLine($"{coffee.GetDescription()} - ${coffee.GetCost()}");

        // Add milk to the coffee
        coffee = new MilkDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()} - ${coffee.GetCost()}");

        // Add sugar to the coffee
        coffee = new SugarDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()} - ${coffee.GetCost()}");
    }
}


Conclusion 

The Decorator Pattern is a great tool for extending functionality dynamically while keeping the base code clean and modular. It is widely used in the .NET Core framework and its libraries to provide flexible and extendable functionality. Some of the usages are:
  • Middleware Pipeline,  app.UseMiddleware()
  • Logging, ILogger with multiple providers (Console, Debug, etc.)
  • Caching, IMemoryCache, IDistributedCache

Comments

Popular posts from this blog

Design Patterns: Builder

This is also, like a Singleton , one of the creational design patterns. It provides the way of creating complex objects step by step by simple chaining and every particular step is independent of other steps. Let us dive into the real example of usage. For showing purpose we have created an example in C# which creates simple SQL queries using described pattern.  using System; using System.Text; namespace BuilderPatternExample { public interface ISqlQueryBuilder { ISqlQueryBuilder Select(string columns); ISqlQueryBuilder From(string table); ISqlQueryBuilder Where(string condition); ISqlQueryBuilder OrderBy(string columns); string Build(); } public class SelectQueryBuilder : ISqlQueryBuilder { private readonly StringBuilder _queryBuilder; public SelectQueryBuilder() { _queryBuilder = new StringBuilder(); } public ISqlQueryBuilder Select(string columns) { ...

Design Patterns: Strategy

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to be selected at runtime, providing flexibility in designing software. It’s particularly useful when you have multiple ways of performing a task, and you want to choose the implementation dynamically without altering the client code. When To Use It? You Have Multiple Algorithms or Behaviors. Use it when you have a need for muplitple ways of performing a task, and you want to make these implementations interchangeable. Examples: Different sorting algorithms, payment methods, discount calculations... You Want to Eliminate Conditional Logic.  If you find yourself writing large if-else or switch statements to decide which algorithm to use, this pattern can simplify and clean up your code. Examples: A game character with different attack styles  You Need Runtime Flexibility.  Use this pattern if the ...

Design Patterns: Singleton

Tyipically the first design pattern most people learn, often wrongly ☺ To give an introduction, we can say that singleton is one of the creational design patterns which ensures only one class instance with single point of access thru entire application.  Because it is relatively simple to implement, the Singleton pattern is sometimes misapplied in situations where it is not the most suitable choice. When to use it? Here are the few examples of corrent usage of singleton: Configuration Management  Centralized configuration settings for consistent use thru entire application Caching Maintaning  Single istance of cached objects for easy and fast acces Logging  Ensure unified mechanism to avoid duplication of log files, formats, etc Global State Management  Centralized management of the state which is needed to be shared accross the application Resource sharing  Thread pools, database connection, I/O operations When not to use it? On the other hand, here are fe...