Part 12 - Decorators
So... Decorators (for those who don't know, or don't remember) are a classic design pattern - have a look at the wikipedia entry if your curious on doing any background reading.
Up until now we've been creating abstractions and swapping implementations, but here we are going to chain implementations... so for this example we have an Order class which contains a bunch of order items, here's how that looks:
Order.cs
public class Order
{
private string _countryCode;
private readonly List_items = new List (); public List
Items
{
get { return _items; }
}public string CountryCode
{
get { return _countryCode; }
set { _countryCode = value; }
}
}
OrderItem.cs
public class OrderItem
{
private string _name;
private bool _isFragile;
private int _quantity;
private decimal _costPerItem;public OrderItem(string name, int quantity, decimal costPerItem, bool isFragile)
{
_name = name;
_quantity = quantity;
_costPerItem = costPerItem;
_isFragile = isFragile;
}public bool IsFragile
{
get { return _isFragile; }
set { _isFragile = value; }
}public int Quantity
{
get { return _quantity; }
set { _quantity = value; }
}public decimal CostPerItem
{
get { return _costPerItem; }
set { _costPerItem = value; }
}public string Name
{
get { return _name; }
set { _name = value; }
}
}
And now we have an interface that a class can implement to calculate the cost of an order:
public interface ICostCalculator
{
decimal CalculateTotal(Order order);
}
And last of all we have an implementation of the cost calculator, which calculates the base cost of an order based on quantities and per-item costs:
public class DefaultCostCalculator : ICostCalculator
{
public decimal CalculateTotal(Order order)
{
decimal total = 0;foreach (OrderItem item in order.Items)
{
total += (item.Quantity*item.CostPerItem);
}return total;
}
}
Pretty simple, so what about the program itself... well here it is, we just have 2 different orders we are calculating the total cost for:
internal class Program
{
private static void Main(string[] args)
{
WindsorContainer container = new WindsorContainer(new XmlInterpreter());Order order1 = new Order();
order1.CountryCode = "NZ";
order1.Items.Add(new OrderItem("water", 10, 1.0m, false));
order1.Items.Add(new OrderItem("glass", 5, 20.0m, true));Order order2 = new Order();
order2.CountryCode = "US";
order2.Items.Add(new OrderItem("sand", 50, 0.2m, false));ICostCalculator costCalculator = container.Resolve
();
Console.WriteLine("Cost to deliver Order 1: {0}", costCalculator.CalculateTotal(order1));
Console.WriteLine("Cost to deliver Order 2: {0}", costCalculator.CalculateTotal(order2));Console.Read();
}
}
Our configuration (predictably) just had the default implementation of the cost calculator registered:
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.DefaultCostCalculator, IoC.Tutorials.Part12" />
Now lets see the output of running it:
Cost to deliver Order 1: 110.0
Cost to deliver Order 2: 10.0
So that seems OK... But I live in New Zealand, and in New Zealand we have GST (Goods and Services Tax) which means that I need to add another 12.5% to the total value of an order for NZ customers... so I could modify the default shipping calculator... but that's going to get ugly fast as more requirements come in (and more configuration needs to be exposed) - so instead I'll create a decorator for the cost calculator, just for doing GST:
public class GstCostCalcualtorDecoarator : ICostCalculator
{
private readonly ICostCalculator _innerCalculator;
private decimal _gstRate = 1.125m;public GstCostCalcualtorDecoarator(ICostCalculator innerCalculator)
{
_innerCalculator = innerCalculator;
}public decimal GstRate
{
get { return _gstRate; }
set { _gstRate = value; }
}private bool IsNewZealand(Order order)
{
return (order.CountryCode == "NZ");
}public decimal CalculateTotal(Order order)
{
decimal innerTotal = _innerCalculator.CalculateTotal(order);if (IsNewZealand(order))
{
innerTotal = (innerTotal*_gstRate);
}return innerTotal;
}
}
Notice it's dependency on ICostCalculator, and that it actually invokes the inner calculator to get a total and then "decorates" the total (by adding the 12.5% when the country for the order is New Zealand).
Now we can actually wire this up in our container without changing any of our application code... let's look at that:
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.GstCostCalcualtorDecoarator, IoC.Tutorials.Part12">${costCalculator.default}
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.DefaultCostCalculator, IoC.Tutorials.Part12" />
Notice that the GSTdecorator is registered first (so it will be the default implementation of the ICostCalculator service) and it's inner calculator is wired to our default cost calculator... but proof is in the pudding, lets see what the results of running the program are now:
Cost to deliver Order 1: 123.7500
Cost to deliver Order 2: 10.0
Great, so our New Zealand order is now ($110 * 1.125 = $123.75) - but our United states order is still at the original $10 dollars... but we forgot to calculate shipping... so let's add another decorator.
public class ShippingCostCalculatorDecorator : ICostCalculator
{
private readonly ICostCalculator _innerCalculator;
private decimal _shippingCost = 5.0m;
private decimal _fragileShippingPremium = 1.5m;public ShippingCostCalculatorDecorator(ICostCalculator innerCalculator)
{
_innerCalculator = innerCalculator;
}public decimal ShippingCost
{
get { return _shippingCost; }
set { _shippingCost = value; }
}public decimal FragileShippingPremium
{
get { return _fragileShippingPremium; }
set { _fragileShippingPremium = value; }
}public decimal CalculateTotal(Order order)
{
decimal innerTotal = _innerCalculator.CalculateTotal(order);
return innerTotal + GetShippingTotal(order);
}private decimal GetShippingTotal(Order order)
{
decimal shippingTotal = 0;foreach (OrderItem item in order.Items)
{
decimal itemShippingCost = ShippingCost*item.Quantity;
if (item.IsFragile) itemShippingCost *= FragileShippingPremium;
shippingTotal += itemShippingCost;
}return shippingTotal;
}
}
And of course, we need to wire that up as well:
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.ShippingCostCalculatorDecorator, IoC.Tutorials.Part12">${costCalculator.gstDecorator}
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.GstCostCalcualtorDecoarator, IoC.Tutorials.Part12">${costCalculator.default}
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.DefaultCostCalculator, IoC.Tutorials.Part12" />
Notice the chain, now we have Shipping -> GST -> Default Calculator .. so running the app again we can see the new totals:
Cost to deliver Order 1: 211.2500
Cost to deliver Order 2: 260.0
Now there's a small problem here... we're calculating GST before shipping, this actually should be the other way round... think about what you would have to do to change this in a program normally - ugh! But of course decorators and IoC containers are a great fit, we'll just change the order of decorators, and while we're at it, lets make GST 20% and no longer charge a premium for shipping fragile goods... easy!
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.GstCostCalcualtorDecoarator, IoC.Tutorials.Part12">${costCalculator.shippingDecorator}
1.20
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.ShippingCostCalculatorDecorator, IoC.Tutorials.Part12">${costCalculator.default}
0.0
service="IoC.Tutorials.Part12.ICostCalculator, IoC.Tutorials.Part12"
type="IoC.Tutorials.Part12.DefaultCostCalculator, IoC.Tutorials.Part12" />
And for anyone sad enough to be checking the results with a calculator (or with the human equivalent ;o) we'll run the program one last time:
Cost to deliver Order 1: 192.0000
Cost to deliver Order 2: 260.0
Now one thing to keep in mind is that so far we've been decorating return values... but you can also decorate methods with logic that occurs before invoking the inner service - a common example of this might be to validate something before it's committed to a repository (and perhaps throwing an exception if it's not OK) - and now suddenly you can turn validation on or off at the configuration level.
Next time we'll have a look at another way of writing this functionality without using decorators, stay tuned.