Overview

Decorator, as described in the GoF book, is one of the most well-known and widely used design patterns in the Object Oriented world.

To quote Wikipedia:

The decorator pattern can be used to extend (decorate) the functionality of a certain object statically, or in some cases at run-time, independently of other instances of the same class, provided some groundwork is done at design time. This is achieved by designing a new decorator class that wraps the original class.”

“This pattern is designed so that multiple decorators can be stacked on top of each other, each time adding a new functionality to the overridden method(s).”

The UML diagram corresponding to this pattern is pretty straightforward:

Decorator UML diagram

Fig. 1 Decorator UML Diagram

As stated in the initial description and as depicted by the diagram, the Decorator essentially wraps the Component via a double relationship: ISA and HASA. It is a Component and it also has one. That way, it can pose as a Component and extend its functionality by intercepting and augmenting all calls made to it.

The careful reader will also notice that the Decorator pattern adheres to the Open/Closed design principle: it keeps the Component open for extension, but closed for modification.

With that in mind, let's go ahead and have a look at one of the most popular concrete examples of where the Decorator Pattern might come in handy.

A sample implementation in C#
Setting the context

Suppose we're trying to model a Coffee shop. Initially, the shop only serves two coffee types: Filtered and Espresso. We're only interested in tracking the Price and Description of our beverages, so the following pair of elementary classes should do:


public class Filtered
{
    public string GetDescription()
    {
        return "Filtered with care";
    }

    public double GetCost()
    {
        return 1.99;
    }
}

public class Espresso
{
    public string GetDescription()
    {
        return "Espresso made with care";
    }

    public double GetCost()
    {
        return 2.99;
    }
}


The problem

This arrangement can't last for long, however, as we realize we need to support adding condiments to our drinks (e.g. Milk and Chocolate, for starters).

First solution

The naïve solution would be to create a pair of new classes for both Filtered and Espresso: FilteredWithChocolate, FilteredWithMilk, EspressoWithChocolate, EspressoWithMilk.

That can seem acceptable at first, but we're missing something – what if a customer wants both Milk and Chocolate on their Espresso? Or on their Filtered, for that matter?

That would generate another pair of classes: FilteredWithChocolateAndMilk, EspressoWithChocolateAndMilk, raising the total to 6.

C# decorator

Figure 2 - First solution leads to class explosion

What if someone likes their coffee with double Milk?

What if we decide to introduce other coffee and condiment types as we go?

It's not hard to see how this initial solution isn't very convenient. At the very least, it leads to a problem that is sometimes referred to as class explosion.

Second solution

Another approach would be to just keep our initial two classes and add flags to them for every possible condiment:


public class Espresso
{
    bool _hasMilk;
    bool _hasChocolate;

    public Espresso(bool hasMilk, bool hasChocolate)
    {
        _hasMilk = hasMilk;
        _hasChocolate = hasChocolate;
    }

    public string GetDescription()
    {
        var description = "Espresso made with care";

        if (_hasMilk)
        {
            description += ", Milk";
        }
        if (_hasChocolate)
        {
            description += ", Chocolate";
        }

        return description;
    }

    public double GetCost()
    {
        var cost = 2.99;

        if (_hasMilk)
        {
            cost += 0.19;
        }
        if (_hasChocolate)
        {
            cost += 0.29;
        }

        return cost;
    }
}

This would solve the class explosion problem at the expense of introducing another one – every time we want to add a new condiment we need to modify all beverage types to accommodate it. More specifically, we need to add a new flag to the beverage classes along with another conditional to account for the new condiment in GetDescription() and GetCost().

This new solution breaks the Open/Closed principle and hints at future maintenance troubles in case we need to extend our product base consistently.

Even more, it doesn't cover for one of the scenarios I've briefly mentioned earlier: what if we need double condiments? Or triple? We still can't do that dynamically.

Enter the Decorator Pattern

The problem we're discussing lends itself well to a Decorator-based solution. Beverages will be Components and we're going to add one CondimentDecorator for each condiment type. These Decorators will be composable, allowing for the dynamic creation of any coffee-condiment combination we need.

First, we need to extract an abstract Coffee type to hold the common functionality that all drinks need to provide - the GetDescription() and GetCost() methods. We'll use an interface for that and call it ICoffee:


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

Next, our two concrete coffee types, Filtered and Espresso, should implement this interface formally.

Then we can finally write our abstract decorator, which we'll call CondimentDecorator:


public abstract class CondimentDecorator : ICoffee
{
    ICoffee _coffee;

    protected string _name = "undefined condiment";
    protected double _price = 0.0;

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

    public string GetDescription()
    {
        return string.Format("{0}, {1}", _coffee.GetDescription(), _name);
    }

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

As expected, CondimentDecorator implements the ICoffee interface while also wrapping an instance of the same type (isa & hasa relationship – remember?).

The calls to GetDescription() get intercepted and the description of the wrapped object is appended to the description of the condiment.

Similarly, the cost of the wrapped object is added to the cost of the condiment in GetCost(). The following diagram illustrates the call sequence that takes place when a GetCost() call is being made on a top-level decorator.

Sequance diagram for top-level Decorator

Figure 3 - Sequence diagram for top-level Decorator

Let's now write the concrete decorators and see how we can use our new code.


public class MilkDecorator : CondimentDecorator
{
    public MilkDecorator(ICoffee coffee)
        :base(coffee)
    {
        _name = "Milk";
        _price = 0.19;
    }
}

public class ChocolateDecorator : CondimentDecorator
{
    public ChocolateDecorator(ICoffee coffee)
        :base(coffee)
    {
        _name = "Chocolate";
        _price = 0.29;
    }
}

The resulting class diagram should look very much like this:

Class Diagram for the Decorator-based solution

Figure 4 - Class Diagram for the Decorator-based solution

We should now be able to write something along the lines of:

[Test]
public void ShouldSupportCondiments()
{
    var beverages = new List
    {
        new ChocolateDecorator(new Filtered()),
        new ChocolateDecorator(new MilkDecorator(new Espresso()))
    };

    var filteredWithChocolate = beverages.First(); 
    Assert.AreEqual("Filtered with care, Chocolate", 
filteredWithChocolate.GetDescription());
    Assert.AreEqual(1.99 + 0.29, filteredWithChocolate.GetCost());

    var espressoWithMilkAndChocolate = beverages.Skip(1).First();
    Assert.AreEqual("Espresso made with care, Milk, Chocolate", 
espressoWithMilkAndChocolate.GetDescription());
    Assert.AreEqual(2.99 + 0.19 + 0.29, espressoWithMilkAndChoco-late.GetCost());
}

Pros

With that, our initial problem is solved in an elegant manner. We:

  • have avoided the class explosion issue
  •  are observing the Open/Closed principle
  •  can stack as many condiments on top of a beverage as we like
    • double and triple milk has never been easier!
Cons

All the advantages listed above were gained at the price of adding a couple of classes to our design. This extra complexity is, however, minimal and completely worth our time.

The only thing that doesn't look so nice about the resulting code is the constructor chain:


new ChocolateDecorator(new MilkDecorator(new Espresso()))

That's a good illustration of the encapsulation process that takes place, but it feels fragile and isn’t composable - a Builder object could come in handy.

Conclusion

We saw a brief formal introduction of the Decorator Pattern and then we had a look at a concrete problem that this construct is a natural fit for. Another example could have been a Pizza shop or a Car or Bike configurator. Anything that deals with objects whose behaviors should be extendable dynamically would do.

A real-life example would be the I/O Stream implementations of both Java and .NET platforms. Here’s a Java Streams-based exercise aimed at helping the reader get familiar with the Decorator pattern.

Share on:

Want to stay on top of everything?

Get updates on industry developments and the software solutions we can now create for a smooth digital transformation.

* I read and understood the ASSIST Software website's terms of use and privacy policy.

Frequently Asked Questions

ASSIST Software Team Members

See the past, present and future of tech through the eyes of an experienced Romanian custom software company. The ASSIST Insider newsletter highlights your path to digital transformation.

* I read and understood the ASSIST Software website's terms of use and privacy policy.

Follow us

© 2024 ASSIST Software. All rights reserved. Designed with love.