Decorator Design Pattern


Here, we will see how to create Decorator Design Pattern with example.

Decorator Design Pattern

The Decorator pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. It’s a structural pattern that uses composition rather than inheritance to extend functionality.

Example- Coffee Odering System

Imagine a coffee shop where customers can customize their coffee by adding different toppings (like milk, sugar, whipped cream, etc.). Instead of creating separate classes for every possible combination of coffee and toppings, we can use the Decorator pattern to dynamically add these toppings to a base coffee object. We will explain in detail lator part of the article.

Decorator Design Pattern

Example - TextEditor

Imagine a TextEditor class that needs additional features like spell check and grammar check. We will aslo explain in detail lator part of the article.

Decorator Design Pattern

Advantages of the Decorator Pattern

  1. Flexibility - You can add functionality to objects dynamically at runtime rather than compile-time.
  2. Promotes Single Responsibility - Each decorator class can focus on a specific behavior, avoiding feature-heavy base classes.
  3. Avoids Subclass Explosion - Rather than creating multiple subclasses to achieve variations in behavior, you create decorators that combine or extend existing functionality.
  4. Reusable Components - Decorators can be reused across different objects, making them modular and reusable.
  5. Combining Behavior - Multiple decorators can be applied to an object in different combinations, providing a powerful way to modify behavior without modifying the underlying object.

When to Use the Decorator Pattern

  • When you want to add responsibilities to objects dynamically and transparently.
  • When you want to avoid subclassing to extend functionality.
  • When it’s necessary to combine behaviors flexibly without creating large inheritance trees.
  • When object responsibilities need to be removed or added dynamically.

Without the Decorator pattern

Imagine a TextEditor class that needs additional features like spell check and grammar check. Using inheritance would create subclasses like TextEditorWithSpellCheck, TextEditorWithGrammarCheck, and TextEditorWithBothChecks. This approach leads to many subclasses.

Without the Decorator pattern, you would typically achieve the same functionality using inheritance or by directly adding the logic to the base class. However, this can lead to some drawbacks, such as tightly coupled code, lack of flexibility, and deep inheritance hierarchies.

Here's how you can implement the functionality without the Decorator pattern.

Using Inheritance

// Base class for Text Editor
using System;

public class BasicTextEditor
{
    public virtual void Display()
    {
        Console.WriteLine("Displaying basic text editor.");
    }
}

// SpellCheck Text Editor (Extends BasicTextEditor)
public class SpellCheckTextEditor : BasicTextEditor
{
    public override void Display()
    {
        base.Display();
        Console.WriteLine("Adding spell check functionality.");
    }
}

// GrammarCheck Text Editor (Extends BasicTextEditor)
public class GrammarCheckTextEditor : BasicTextEditor
{
    public override void Display()
    {
        base.Display();
        Console.WriteLine("Adding grammar check functionality.");
    }
}

// Editor with Both Spell Check and Grammar Check (Inheritance Chain)
public class FullFeatureTextEditor : SpellCheckTextEditor
{
    public override void Display()
    {
        base.Display();
        Console.WriteLine("Adding grammar check functionality.");
    }
}

// Usage
class Program
{
    static void Main()
    {
        // Example of using inheritance approach for specific functionality
        BasicTextEditor editor = new FullFeatureTextEditor();
        editor.Display();
    }
}

Output

Decorator Design Pattern

Issues with This Approach

  1. Class Explosion - If you need various combinations of features (e.g., only spell check, only grammar check, both), you'll need multiple classes (SpellCheckTextEditor, GrammarCheckTextEditor, FullFeatureTextEditor), which leads to a large number of subclasses.
  2. Inflexibility - Once you've created a subclass, its behavior is fixed at compile-time. You can't add or remove features dynamically at runtime.
  3. Tight Coupling - The new functionality (spell check, grammar check) is tightly coupled to the subclass, making it harder to change independently.

With the Decorator Pattern

The above approach using inheritance would create subclasses like TextEditorWithSpellCheck, TextEditorWithGrammarCheck, and TextEditorWithBothChecks. This approach leads to many subclasses. To overcome that problem you can use Decorator design pattern.

Using the Decorator pattern

// Step 1: Define the ITextEditor interface
using System;

public interface ITextEditor
{
    void Display();
}

// Step 2: Basic Text Editor implementation
public class BasicTextEditor : ITextEditor
{
    public void Display()
    {
        Console.WriteLine("Displaying basic text editor.");
    }
}

// Step 3: Abstract Decorator class implementing ITextEditor
public abstract class TextEditorDecorator : ITextEditor
{
    protected ITextEditor _textEditor;

    public TextEditorDecorator(ITextEditor textEditor)
    {
        _textEditor = textEditor;
    }

    public virtual void Display()
    {
        _textEditor.Display();
    }
}

// Step 4: Concrete Decorators - SpellCheck and GrammarCheck
public class SpellCheckDecorator : TextEditorDecorator
{
    public SpellCheckDecorator(ITextEditor textEditor) : base(textEditor) { }

    public override void Display()
    {
        base.Display();
        Console.WriteLine("Adding spell check functionality.");
    }
}

public class GrammarCheckDecorator : TextEditorDecorator
{
    public GrammarCheckDecorator(ITextEditor textEditor) : base(textEditor) { }

    public override void Display()
    {
        base.Display();
        Console.WriteLine("Adding grammar check functionality.");
    }
}

// Step 5: Usage of the Decorator Pattern
class Program
{
    static void Main()
    {
        ITextEditor editor = new BasicTextEditor(); // Basic editor
        editor = new SpellCheckDecorator(editor);   // Adding spell check
        editor = new GrammarCheckDecorator(editor); // Adding grammar check
        editor.Display();
    }
}

Output

Decorator Design Pattern

Real-World Example - Coffee Ordering System

Imagine a coffee shop where customers can customize their coffee by adding different toppings (like milk, sugar, whipped cream, etc.). Instead of creating separate classes for every possible combination of coffee and toppings, we can use the Decorator pattern to dynamically add these toppings to a base coffee object.

Coffee Example Using Decorator Pattern

// Step 1: Create a Coffee interface
using System;

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

// Step 2: Basic Coffee implementation
public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple Coffee";

    public double GetCost() => 5.0;
}

// Step 3: Abstract Decorator class
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();
    }
}

// Step 4: Concrete Decorators (Milk, Sugar, Whipped Cream)
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;
    }
}

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;
    }
}

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

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

    public override double GetCost()
    {
        return _coffee.GetCost() + 2.0;
    }
}

// Step 5: Usage of the Decorator Pattern
class Program
{
    static void Main()
    {
        ICoffee coffee = new SimpleCoffee();
        Console.WriteLine($"{coffee.GetDescription()} costs {coffee.GetCost()}");

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

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

        // Add whipped cream to the coffee
        coffee = new WhippedCreamDecorator(coffee);
        Console.WriteLine($"{coffee.GetDescription()} costs {coffee.GetCost()}");
    }
}

Output

Decorator Design Pattern

Applications of the Decorator Design Pattern

  • UI frameworks: Decorating UI components with features like scroll bars, borders, or background colors.
  • Logging Frameworks: Dynamically adding logging behavior to various methods or services.
  • Streams in I/O Operations: Wrapping input/output streams in decorators to provide features like buffering, compression, or encryption.
  • Enhancing APIs: Dynamically adding caching, retry logic, or metrics gathering to APIs.

Prev Next

Top Articles

  1. What is JSON
  2. How to convert a javaScript object in JSON object
  3. Some Important JSON Examples
  4. Common JSON Interview Question