Saturday, January 14, 2012

What is the Open Closed Principle in OO

What does this mean, well the title refers to classes and means:
Open to Extension yet Closed for modification.

So we should be able to extend it if we need to add behaviour, but we should not need to rip the code apart.

To do this we need to use abstractions. These are Interfaces and Abstract classes, or in procedural languages we can use parameters.


The problem

A common problem in coding is when an object has some kind of rule inside it. Perhaps there's an If else -if  - else if ... inside. And each If statement is really a rule.

A typical example is the pricing cart and product pricing problem.
If we sell products but they have different pricing rules, then we may need to add a new rule after a while.

As an example, say we have a class that sums the price of products in a shopping cart but each product is priced in a different currency. (This example is a good example of a problem, but the solution given here would probably be very different, so don't take it too seriously or literally. It's the principle that we're after.)


namespace OpenClosedPrinciple.FirstAttempt
{

    public class OrderItem
    {
        public string Currency { get; set; }
        public decimal Price { get; set; }
    }

    public class OpenClosedPrincipleDemo1
    {
        private List<OrderItem> _items;

        public OpenClosedPrincipleDemo1()
        { 
            _items = new List<OrderItem>();
        }

        public void AddItem(OrderItem item){
            _items.Add(item);
        }

        public decimal CalculateTotalAmountInEuros()
        {
            decimal total = 0m;

            foreach (OrderItem item in _items)
            {
                if (item.Currency == "Euro")
                {
                    total += item.Price;
                }
                else if (item.Currency == "US dollar")
                {
                    total += item.Price * (decimal)1.26;
                }
                else if (item.Currency == "Brazil Real")
                {
                    total += item.Price * (decimal)2.8;
                }
                // We'll probably end up having a lot more of these...
            }
            return total;
        }
    }
}


We can see that the if-else if - else if could become quite long if we wanted to add new currencies and any change would require a code change.
We can come up with another version by using the Strategy pattern. I have a post on the Strategy pattern.

namespace OpenClosedPrinciple.SecondAttempt
{
    public class OrderItem
    {
        public string Currency { get; set; }
        public decimal Price { get; set; }
    }

    public interface IPriceCalculator
    {
        decimal Calculate(OrderItem item);
    }

    public interface IPriceConversionRule
    {
        bool IsMatch(OrderItem item);
        decimal CalculatePrice(OrderItem item);
    }

    public class BrazilConversionRule : IPriceConversionRule
    {
        public bool IsMatch(OrderItem item)
        {
            return (item.Currency == "Brazil Real") ? true : false;
        }
        public decimal CalculatePrice(OrderItem item)
        {
            return item.Price * (decimal)2.8;
        }
    }

    public class EuroConversionRule : IPriceConversionRule
    {
        public bool IsMatch(OrderItem item)
        {
            return (item.Currency == "Euro") ? true : false;
        }
        public decimal CalculatePrice(OrderItem item)
        {
            return item.Price;
        }
    }

    public class USDollarConversionRule : IPriceConversionRule
    {
        public bool IsMatch(OrderItem item)
        {
            return (item.Currency == "US dollar") ? true : false;
        }
        public decimal CalculatePrice(OrderItem item)
        {
            return item.Price * (decimal)1.26;
        }
    }

    public class PriceCalculator : IPriceCalculator
    {
        private readonly List<IPriceConversionRule> _priceRules;

        public PriceCalculator()
        {
            _priceRules = new List<IPriceConversionRule>();
            _priceRules.Add(new BrazilConversionRule());
            _priceRules.Add(new USDollarConversionRule());
            _priceRules.Add(new EuroConversionRule());
        }

        public decimal Calculate(OrderItem item)
        {
            return _priceRules.First(rule => rule.IsMatch(item)).CalculatePrice(item);
        }
    }

    public class OpenClosedPrincipleDemo2
    {
        private readonly List<OrderItem> _items;
        private readonly IPriceCalculator _calculator;

        public OpenClosedPrincipleDemo2()
            : this(new PriceCalculator())
        { }

        public OpenClosedPrincipleDemo2(IPriceCalculator calculator)
        {
            _calculator = calculator;
            _items = new List<OrderItem>();
        }

        public void AddItem(OrderItem item)
        {
            _items.Add(item);
        }

        public decimal CalculateTotalAmountInEuros()
        {
            decimal total = 0;
            foreach (var item in _items)
            {
                total += _calculator.Calculate(item);
            }
            return total;
        }
    }
}


Complexity

107 lines versus 48 lines. From that view the fancy strategy pattern is longer and if we were after a quick piece of code then I would imagine most people would prefer the shorter version.

Complexity of the main class is less in the longer version. It is clear to see what it is doing and it is doing it well. It is doing one thing and has delegated the task of applying rules to someone else. The longer version is better form that perspective.

Understanding the rules. Well that is more difficult to understand on the longer version. But, if the rules were complex and if they differed greatly from one to another, then the longer code version would separate these really well and would aid in maintenance.


No comments:

Post a Comment