DEV Community

Esra ŞAHİN
Esra ŞAHİN

Posted on

THE DECORATOR PATTERN USING .NET

I guess, you have happened upon the "SOLID" principles, while surfing Object Oriented Programming World. I won't give details of the SOLID Principles. But I want to remind you of the second principle.
(S: Single Responsibility Principle, O: Open Closed Principle, etc.)

Reminder:
Open Closed Principle
Classes should be open for extension, but closed for modification.

It means that when you create classes, you should design extensible classes. So, you can include new behaviors in classes (extend) but you can't change actual behaviors in these classes.
OC Principle can't be possible for all classes. Sometimes, applying the OC principle can increase the complexity of the code. But if you know the domain of the code and can guess future changes, you can apply the OC principle to maintain code easily.
Okey. This was the reminder part. What is the relation OC principle to the Decorator Pattern? Let's learn together through an example.

PROJECT: You want to create a project that calculate coffee cost according to its ingredients.
Coffe Types: House Blend, Dark Roast, Decaf, Espresso
Condiments: Milk, soy, chocolate (otherwise known as mocha) etc.
How can we design our app?

Image description

public abstract class Beverages
    {

        private bool milk, soy, mocha = false;
        public double Price { get; set; }
        public abstract string GetDescription();

        public void Cost()
        {
            if (milk)
                this.Price += .10;
            if (mocha)
                this.Price += .20;
            if (soy)
                this.Price += .15;
        }
        public void SetMilk()
        {
            this.milk = true;
        }
        public void SetSoy()
        {
            this.soy = true;
        }
        public void SetMocha()
        {
            this.mocha = true;
        }

    }

    public class DarkRoast : Beverages
    {
        public override string GetDescription()
        {
            return "DarkRoast Coffee";
        }
        public new void Cost()
        {
            base.Cost();
            this.Price += .99;
        }
    }
Enter fullscreen mode Exit fullscreen mode
private static void Main(string[] args)
    {
        DarkRoast darkRoast = new();
        darkRoast.SetMocha();
        darkRoast.SetMilk();
        darkRoast.Cost();
        Console.WriteLine(darkRoast.GetDescription()+" Price: "+darkRoast.Price);

    }
Enter fullscreen mode Exit fullscreen mode

This code is terrible in terms of maintainability. Why?

  • If the condiment's price changes, Superclass (beverage) must change. Oops, It is not coherent with OC Principle.

  • In the future, If a new beverage, like iced tea, is included in this app. Condiments (soy, milk, etc) won't be appropriate for iced tea.

How can we refactor this code?
Answer: By Using The Decorator Pattern

"The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality." [1]

We've seen that representing our beverage plus condiment pricing scheme with inheritance has not worked out very well. So, here's what we'll do instead: we'll start with a beverage and "decorate" it with the condiments at runtime. For example, if the customer wants a Dark Roast with Mocha and Milk, then we'll;
· Take a DarkRoast object
· Decorate it with a Mocha object
· Decorate it with a Milk object
· Call the cost() method and rely on the delegation to add on the condiment costs
NOTE: think of decorator objects as "wrappers." Let's see how this works.

Image description

  • DarkRoast is a beverage. It inherits the beverage class and has a "Cost" method.

  • Mocha object is a decorator. Its type is as same Beverage type. (Polymorfizm) So, Mocha has a cost() method too, and through polymorphism, we can treat any Beverage wrapped in Mocha as a Beverage.

  • Milk(Whip) is a decorator, so it also mirrors DarkRoast's type and includes a cost() method.
     
     So, a DarkRoast wrapped in Mocha and Milk is still a Beverage and we can do anything with it we can do with a DarkRoast.

Steps:
· We start with our DarkRoast object
· The customer wants DarkRoast, so we create a Mocha object and wrap it around the DarkRoast.
· The customer also wants Milk, so we create a Milk decorator and wrap a Mocha with it
· To compute the cost, We call cost() on the outermost decorator, Milk, and Milk is going to delegate computing the cost to the objects it decorates. Once it gets a cost, it will add on the cost of the Milk.
Let's code it.

public abstract class Beverage
    {

        public abstract double Cost();
        public virtual string GetDescription()
        {
            return "unknown";
        }
    }

    public class DarkRoast : Beverage
    {
        public override string GetDescription()
        {
            return "Dark Roast";
        }

        public override double Cost()
        {
            return .99;
        }

    }
    public class HouseBlend : Beverage
    {
        public override string GetDescription()
        {
            return "House Blend";
        }
        public override double Cost()
        {
            return .89;
        }
    }

Enter fullscreen mode Exit fullscreen mode
public abstract class CondimentDecorator : Beverage
    {
        protected Beverage beverage;
        public CondimentDecorator(Beverage beverage)
        {
            this.beverage = beverage;
        }
        public override abstract Double Cost();
        public override abstract string GetDescription();
    }

    public class Mocha : CondimentDecorator
    {

        public Mocha(Beverage beverage) : base(beverage)
        {
        }
        public override double Cost()
        {
            return beverage.Cost() + .20;
        }

        public override string GetDescription()
        {

            return beverage.GetDescription() + " ,Mocha";
        }
    }
    public class Soy : CondimentDecorator
    {

        public Soy(Beverage beverage) : base(beverage)
        { }
        public override double Cost()
        {
            return beverage.Cost() + .15;
        }

        public override string GetDescription()
        {

            return beverage.GetDescription() + " ,Soy";
        }
    }
    public class Milk : CondimentDecorator
    {

        public Milk(Beverage beverage) : base(beverage)
        {

        }
        public override double Cost()
        {
            return beverage.Cost() + .10;
        }

        public override string GetDescription()
        {

            return beverage.GetDescription() + ", Milk";
        }
Enter fullscreen mode Exit fullscreen mode
private static void Main(string[] args)
    {
        Beverage darkRoast = new DarkRoast();

        //Wrap dark roast coffee with condiments.
        //Dark Roast with milk
        Milk milkDarkRoast = new Milk(darkRoast);
        Console.WriteLine(milkDarkRoast.GetDescription()+ " Cost:"+milkDarkRoast.Cost());

        // Dark Roast with milk and mocha

        Beverage mochaMilkDarkRoast = new Mocha(milkDarkRoast);
        Console.WriteLine(mochaMilkDarkRoast.GetDescription() + " Cost:" + mochaMilkDarkRoast.Cost());
    }
Enter fullscreen mode Exit fullscreen mode

Output:
Dark Roast, Milk Cost:1.09
Dark Roast, Milk ,Mocha Cost:1.29
If this article is helpful to you, you can say "thanks" by clicking claps and following me :) Thank you.
Sources:
"Head First Design Patterns" book by Eric Freeman (Author), Bert Bates (Author), Katy Siera (Author)

Top comments (0)