Thursday, August 23, 2012

Refactoring inheritence explosion to fleunt decorator


The decorator pattern is used to extend or alter the functionality of an object at runtime. IT does this by wrapping the object with a decorator class, leaving the original object intact without modification. Let’s look into the code structure given below for a Starbucks coffee menu implementation for calculating the cost of the beverages including the various coffee toppings provided. An inheritance based approach will result in an object explosion like this.

We can try to reduce the complexity of this design by refactoring this to a decorator implementation. Later I’ll show how we can use fluent interfaces to create a fluent decorator that is more readable and a cleaner approach to use.  The final implementation changes our code structure to something like

We’ll start our refactoring task by creating a base class for our beverage decorator as given below.
public abstract class Beverage
{
    public string Name { get; set; }

    public abstract string GetDetails();
    public abstract int GetCalories();
    public abstract decimal GetCost();
    public abstract string GetDescription();
}
public abstract class BeverageDecorator : Beverage
{
    protected Beverage _beverage;

    protected BeverageDecorator(Beverage beverage)
    {
        _beverage = beverage;
    }

    public override string GetDetails()
    {
        var descriptionBuilder = new StringBuilder();
        descriptionBuilder.Append(_beverage.Name);
        descriptionBuilder.AppendLine(_beverage.GetDescription());
        descriptionBuilder.AppendFormat(" with : {0}", Name);
        descriptionBuilder.AppendLine();
        descriptionBuilder.AppendFormat("Costs : {0:C}", _beverage.GetCost() + GetCost());
        descriptionBuilder.AppendLine();
        descriptionBuilder.AppendFormat("Total calories : {0}g", _beverage.GetCalories() + GetCalories());
        descriptionBuilder.AppendLine(_beverage.GetDetails());
        descriptionBuilder.AppendLine(GetDescription());
        return descriptionBuilder.ToString();
    }
}
We include a protected Beverage member in our decorator to access it and apply extensions on the methods.
Next you can start creating the decorators for the coffee toppings. I’ve added a sample implementation for the whip cream decorator below
public class CreamDecorator : BeverageDecorator
{
    public CreamDecorator(Beverage beverage) : base(beverage)
    {
        Name = "Whip cream";
    }

    public override int GetCalories()
    {
        return 100 + _beverage.GetCalories();
    }

    public override decimal GetCost()
    {
        return _beverage.GetCost() + .50M;
    }

    public override string GetDescription()
    {
        return "Sweetened and flavored with vanilla.";
    }
}
You can now use the beverage object with added toppings as given below
[TestMethod]
public void DecoratorAddsFunctionalityAtRuntime()
{
    Beverage beverage = new CafeMisto();
    beverage = new CreamDecorator(beverage);
    beverage = new ChocolateFlakesDecorator(beverage);
    beverage = new CinnamonSprinklesDecorator(beverage);

    Assert.IsTrue(beverage.GetCost() > 2M);
    Assert.IsTrue(beverage.GetCalories() > 110);
}

Next we can use fluent interfaces approach by adding extension methods to the Beverage class as
public static class BeverageDecoratorExtensions
{
        public static Beverage AddCream(this Beverage beverage)
        {
            return new CreamDecorator(beverage);
        }

        public static Beverage AddChocolateFlakes(this Beverage beverage)
        {
            return new ChocolateFlakesDecorator(beverage);
        }

        public static Beverage AddCinnamonSprinkles(this Beverage beverage)
        {
            return new CinnamonSprinklesDecorator(beverage);
        }
}

After applying fluent interfaces our decorator implementation can be applied by using the syntax
[TestMethod]
public void DecoratorAddsFunctionalityAtRuntime()
{
    var beverage = new CafeMisto()
                        .AddCream()
                        .AddChocolateFlakes()
                        .AddCinnamonSprinkles();

    Assert.IsTrue(beverage.GetDetails().Contains("Whip cream"));
    Assert.IsTrue(beverage.GetCost() > 2M);
    Assert.IsTrue(beverage.GetCalories() > 110);
}

No comments: