decorator

The Decorator Pattern

decorator

Photo by Austin Wade on Unsplash

 

Today I want to take a closer look at the Decorator Pattern.

Sometimes we want to add new behaviour to an existing class without changing that class. Maybe the class definition originates from a third-party package, and we cannot modify it. Or it’s a base class inside a deep inheritance hierarchy, and we do not want all child classes inheriting the new action. 

A few facts about the Decorator pattern:

  • It’s a structural pattern. We use it to compose classes and objects into larger structures with more complex behaviours. 
  • This pattern works by wrapping the existing, immutable class and putting the new behaviours into the wrapper.
  • It’s a pattern that aligns with the Open-Closed Principle—We write new code rather than modify existing code.

Let’s look at an example of the Decorator pattern. 

We are working with a cafe point-of-sale application. The abstract base class for all the different coffees sold is Coffee:

  public abstract class Coffee
  {
     public abstract string Description { get; }
     public abstract decimal Cost { get; }
  }

Two of Coffee‘s inheritors are Espresso and Mocha:

  public class Espresso : Coffee
  {
     public override string Description => "Espresso";
     public override decimal Cost => 4.50m;
  }

  public class Mocha : Coffee
  {
     public override string Description => "Mocha";
     public override decimal Cost => 5.00m;
  }

Cafe owners using the software have requested a change to the program: Customers can modify their coffees with extras condiments, like whipped cream, chocolate powder or a cherry on top. Cafes would be able to charge an additional fee for each dressing, say:

  • Whipped Cream: $1.00
  • Chocolate Powder: $0.50
  • Cherry: $0.75

In the application code, we want to model these modified or ‘decorated’ coffees. To this end, we will employ the Decorator pattern.

Let’s create a new abstract class CoffeeDecorator that implements Coffee (i.e. it’s a Coffee). The constructor for CoffeeDecorator takes a Coffee type: 

  public abstract class CoffeeDecorator : Coffee
  {
     protected Coffee Coffee { get; private set; }

     protected CoffeeDecorator(Coffee coffee)
     {
        Coffee = coffee;
     }
  }

CoffeeDecorator represents the wrapper for the Coffee class. It’s here that we’ll add our new behaviour for the various condiments. Classes WhippedCream, ChocolatePowder and Cherry extend CoffeeDecorator:

  public class WhippedCream : CoffeeDecorator
  {
     public WhippedCream(Coffee coffee) : base(coffee)
     { }

     public override string Description => $"{Coffee.Description} and Whipped Cream";
     public override decimal Cost => Coffee.Cost + 1.00m;
  }

  public class ChocolatePowder : CoffeeDecorator
  {
     public ChocolatePowder(Coffee coffee) : base(coffee)
     { }

     public override string Description => $"{Coffee.Description} and Chocolate Powder";
     public override decimal Cost => Coffee.Cost + 0.50m;
  }

  public class Cherry : CoffeeDecorator
  {
     public Cherry(Coffee coffee) : base(coffee)
     { }

     public override string Description => $"{Coffee.Description} and Cherry on top";
     public override decimal Cost => Coffee.Cost + 0.75m;
  }

Each condiment implementation has a constructor taking a Coffee as a parameter. This mechanism allows us to extend the passed-in Coffee whether it already has condiments or not:

  class Program
  {
     static void Main(string[] args)
     {
        Log(new Espresso());
        Log(new WhippedCream(new Espresso()));
        Log(new Cherry(new WhippedCream(new Espresso())));
        Log(new ChocolatePowder(new Cherry(new WhippedCream(new Mocha()))));

        Console.ReadLine();
     }

     private static void Log(Coffee coffee)
     {
        Console.WriteLine($"Your coffee is {coffee.Description} costing ${coffee.Cost}.");
     }
  }

The output on the console is

  Your coffee is Espresso costing $4.50.
  Your coffee is Espresso and Whipped Cream costing $5.50.
  Your coffee is Espresso and Whipped Cream and Cherry on top costing $6.25.
  Your coffee is Mocha and Whipped Cream and Cherry on top and Chocolate Powder costing $7.25.

That’s pretty cool! Each of the coffees has been modified by the applied condiments. The coffee description and cost reflect the coffee type and all the extras.

In reality, I don’t use the Decorator pattern all that often. I believe I last used it a few years ago when a class I did not want to change required its actions logged to multiple media. From memory, I decorated the logging behaviour onto the class. In that use case, the Decorator pattern made sense. However, often it does not, or at least more flexible alternatives are available to you, like Aggregation or Composition. One of the most problematic aspects of this pattern is the use of inheritance which can be inflexible.

 

Hat tip to Kenji for reminding me of the Decorator Pattern and showing me this example.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply