Thursday, November 7, 2013

Refactoring classes to Strategy and State design patterns

Hi,
Today i try use 2 behavioral design pattern: Strategy and State design patterns. About this patterns there a lot of stuff in net. I wish try use some refactoring, for moving poor designed class to better designed, by using Strategy and State design patterns.

In my example i do calculations of taxes for products depend on season. For example, in Summer, tax for product, which costs 100 units not same, as a tax in Winter for product, which costs 150 units. So, there is system of tax calculation, depends on season and product price.

Lets design!

Bang!

namespace SeasonTaxesNoDP

{
    public class SeasonTaxes
    {
        public enum Season { Spring, Summer, Autumn, Winter, }

        Season _season;

        public SeasonTaxes()
        {
            _season = Season.Spring;
        }

        public void NextSeason()
        {
            switch (_season)
            {
                case Season.Autumn:
                    _season = Season.Winter;
                    break;
                case Season.Spring:
                    _season = Season.Summer;
                    break;
                case Season.Summer:
                    _season = Season.Autumn;
                    break;
                case Season.Winter:
                    _season = Season.Spring;
                    break;
            }
        }

        public void PreviousSeason()
        {
            switch (_season)
            {
                case Season.Autumn:
                    _season = Season.Summer;
                    break;
                case Season.Spring:
                    _season = Season.Winter;
                    break;
                case Season.Summer:
                    _season = Season.Spring;
                    break;
                case Season.Winter:
                    _season = Season.Autumn;
                    break;
            }
        }

        public double CalculateTax(double productPrice)
        {
            switch (_season)
            {
                case Season.Autumn:
                    if (productPrice > 180)
                    {
                        return productPrice * 0.32 + 11.2;
                    }
                    else
                    {
                        return productPrice * 0.32 + 13.2;
                    }
                case Season.Spring:
                    if (productPrice > 170)
                    {
                        return productPrice * 0.52 + 15.2;
                    }
                    else
                    {
                        return productPrice * 0.42 + 17.2;
                    }
                case Season.Summer:
                    if (productPrice > 300)
                    {
                        return productPrice * 0.72 + 21.2;
                    }
                    else
                    {
                        return productPrice * 0.42 + 23.2;
                    }
                case Season.Winter:
                    if (productPrice > 220)
                    {
                        return productPrice * 0.342 + 11.2;
                    }
                    else
                    {
                        return productPrice * 0.562 + 16.2;
                    }
            }
            return 0.0;
        }
    }
}

Using:
        private static void f4()
        {
            SeasonTaxesNoDP.SeasonTaxes season = new SeasonTaxesNoDP.SeasonTaxes();
            season.CalculateTax(345);

            season.NextSeason();
            season.CalculateTax(567);
        }

SeasonTaxes is an Finite-state machine (FSM) of seasons + system of calculations, so it do 2 things. So it have 2 axis of changes: if there change system of calculations it will change or if there change in FSM it will change too. So, it violates The Single Responsibility Principle (SRP).

This code is hard to understand and hard to maintenance.

If i will add new season, it will violate Open - Close principle (OCP)
If i will remove season, same.
Same about system of calculations.

Lets do refactoring.
I will move to State design pattern, so i wish move the FSM out from the class. It will remove one axis of potential changes: changes in Seasons switch machine


Code:
namespace SeasonTaxesStateDP
{
    public abstract class SeasonState
    {
        public abstract void NextSeason(SeasonTaxes seasonTaxes);
        public abstract void PreviousSeason(SeasonTaxes seasonTaxes);
        public abstract double CalculateTax(double productPrice);
    }

    public class SpringSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SummerSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new WinterSeasonState(); }

        public override double CalculateTax(double productPrice)
        {
            if (productPrice > 170)
            {
                return productPrice * 0.52 + 15.2;
            }
            else
            {
                return productPrice * 0.42 + 17.2;
            }
        }
    }

    public class AutumnSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new WinterSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SummerSeasonState(); }

        public override double CalculateTax(double productPrice)
        {
            if (productPrice > 180)
            {
                return productPrice * 0.32 + 11.2;
            }
            else
            {
                return productPrice * 0.32 + 13.2;
            }
        }
    }

    public class SummerSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new AutumnSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SpringSeasonState(); }

        public override double CalculateTax(double productPrice)
        {
            if (productPrice > 300)
            {
                return productPrice * 0.72 + 21.2;
            }
            else
            {
                return productPrice * 0.42 + 23.2;
            }
        }
    }

    public class WinterSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SpringSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new AutumnSeasonState(); }

        public override double CalculateTax(double productPrice)
        {
            if (productPrice > 220)
            {
                return productPrice * 0.342 + 11.2;
            }
            else
            {
                return productPrice * 0.562 + 16.2;
            }
        }
    }

    public class SeasonTaxes
    {
        public SeasonState State { get; set; }

        public SeasonTaxes()
        {
            State = new SpringSeasonState();
        }

        public void NextSeason()
        {
            State.NextSeason(this);
        }

        public void PreviousSeason()
        {
            State.PreviousSeason(this);
        }

        public double CalculateTax(double productPrice)
        {
            return State.CalculateTax(productPrice);
        }
    }
}


Class SeasonTaxes get smaller and simpler. It is delegates responsibilities to SeasonState implementations. 
But, each implementation of SeasonState  violates OCP and SRP. 

So, i did not like calculations in State classes. 
It is looks, like a mess.
I wish move tax calculations out from FSM.
Lets do refactoring to Strategy design pattern:


Code:
namespace SeasonTaxesStateStrategyDP
{
    public interface IStrategyTax
    {
        double CalculateTax(double productPrice);
    }

    public class SpringStrategyTax : IStrategyTax
    {
        public double CalculateTax(double productPrice)
        {
            if (productPrice > 170)
            {
                return productPrice * 0.52 + 15.2;
            }
            else
            {
                return productPrice * 0.42 + 17.2;
            }
        }
    }

    public class AutumnStrategyTax : IStrategyTax
    {
        public double CalculateTax(double productPrice)
        {
            if (productPrice > 180)
            {
                return productPrice * 0.32 + 11.2;
            }
            else
            {
                return productPrice * 0.32 + 13.2;
            }
        }
    }

    public class SummerStrategyTax : IStrategyTax
    {
        public double CalculateTax(double productPrice)
        {
            if (productPrice > 300)
            {
                return productPrice * 0.72 + 21.2;
            }
            else
            {
                return productPrice * 0.42 + 23.2;
            }
        }
    }

    public class WinterStrategyTax : IStrategyTax
    {
        public double CalculateTax(double productPrice)
        {
            if (productPrice > 220)
            {
                return productPrice * 0.342 + 11.2;
            }
            else
            {
                return productPrice * 0.562 + 16.2;
            }
        }
    }

    public abstract class SeasonState
    {
        public abstract void NextSeason(SeasonTaxes seasonTaxes);
        public abstract void PreviousSeason(SeasonTaxes seasonTaxes);

        protected IStrategyTax _strategyTax;

        public double CalculateTax(double productPrice)
        {
            return _strategyTax.CalculateTax(productPrice);
        }
    }

    public class SpringSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SummerSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new WinterSeasonState(); }
    }

    public class AutumnSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new WinterSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SummerSeasonState(); }
    }

    public class SummerSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new AutumnSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SpringSeasonState(); }
    }

    public class WinterSeasonState : SeasonState
    {
        public override void NextSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new SpringSeasonState(); }
        public override void PreviousSeason(SeasonTaxes seasonTaxes) { seasonTaxes.State = new AutumnSeasonState(); }
    }

    public class SeasonTaxes
    {
        public SeasonState State { get; set; }

        public SeasonTaxes()
        {
            State = new SpringSeasonState();
        }

        public void NextSeason()
        {
            State.NextSeason(this);
        }

        public void PreviousSeason()
        {
            State.PreviousSeason(this);
        }

        public double CalculateTax(double productPrice)
        {
            return State.CalculateTax(productPrice);
        }
    }
}

that's it

No comments:

Post a Comment