The Bitter Coder Tutorials, Binsor Style: Part XII, Decorators

Image via Wikipedia
public class Order
{
private readonly List<OrderItem> _items = new List<OrderItem>();
public List<OrderItem> Items
{
get { return _items; }
}
public string CountryCode { get; set; }
}
public class OrderItem
{
public OrderItem(string name, int quantity, decimal costPerItem, bool isFragile)
{
Name = name;
Quantity = quantity;
CostPerItem = costPerItem;
IsFragile = isFragile;
}
public bool IsFragile { get; set; }
public int Quantity { get; set; }
public decimal CostPerItem { get; set; }
public string Name { get; set; }
}
public interface ICostCalculator
{
decimal CalculateTotal(Order order);
}
public class DefaultCostCalculator:ICostCalculator
{
public decimal CalculateTotal(Order order)
{
return order.Items.Sum(ord => ord.CostPerItem*ord.Quantity);
}
}
component "default.calculator", ICostCalculator,DefaultCostCalculatorNow, the program itself:
static void Main(string[] args)
{
var order1 = new Order
{
CountryCode = "NZ",
Items =
{
new OrderItem("water", 10, 1.0m, false),
new OrderItem("glass", 5, 20.0m, true)
}
};
var order2 = new Order
{
CountryCode = "US",
Items =
{
new OrderItem("sand", 50, 0.2m, false)
}
};
var _calc = container.Resolve<ICostCalculator>();
Console.WriteLine("Cost to deliver Order 1: {0}", _calc.CalculateTotal(order1));
Console.WriteLine("Cost to deliver Order 2: {0}", _calc.CalculateTotal(order2));
}
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; }
}
#region ICostCalculator Members
public decimal CalculateTotal(Order order)
{
decimal innerTotal = _innerCalculator.CalculateTotal(order);
if (IsNewZealand(order))
{
innerTotal = (innerTotal*_gstRate);
}
return innerTotal;
}
#endregion
private bool IsNewZealand(Order order)
{
return (order.CountryCode == "NZ");
}
}
component "gst.calculator", ICostCalculator, GstCostCalculatorDecorator: innerCalculator=@default.calculator component "default.calculator", ICostCalculator,DefaultCostCalculatorWe put the GST calculator first, so it's the default implementation, and run the program: Cost to deliver Order 1: 123.7500 Cost to deliver Order 2: 10.0 So, the US one hasn't changed, as expected. Finally, let's add a shipping calculator:
public class ShippingCostCalculatorDecorator : ICostCalculator
{
private readonly ICostCalculator _innerCalculator;
public decimal ShippingCost { get; set; }
public decimal FragileShippingPremium { get; set; }
public ShippingCostCalculatorDecorator(ICostCalculator innerCalculator)
{
_innerCalculator = innerCalculator;
ShippingCost = 5.0m;
FragileShippingPremium = 1.5m;
}
#region ICostCalculator Members
public decimal CalculateTotal(Order order)
{
decimal innerTotal = _innerCalculator.CalculateTotal(order);
return innerTotal + GetShippingTotal(order);
}
#endregion
private decimal GetShippingTotal(Order order)
{
return order.Items.Sum(item =>
{
var itemShippingCost = ShippingCost*item.Quantity;
if (item.IsFragile) itemShippingCost *= FragileShippingPremium;
return itemShippingCost;
});
}
}
component "shipping.calculator", ICostCalculator, ShippingCostCalculatorDecorator: innerCalculator=@gst.calculatorRun it. Cost to deliver Order 1: 211.2500 Cost to deliver Order 2: 260.0 As Alex points out, we are calculating our GST before shipping, which is kinda stoopid. The Decorator Pattern saves our bacon here, as we can just swap things around (oh, and we change some parameters too):
component "gst.calculator", ICostCalculator, GstCostCalculatorDecorator: innerCalculator=@gst.calculator GstRate=1.20 component "shipping.calculator", ICostCalculator, ShippingCostCalculatorDecorator: innerCalculator=@default.calculator FragileShippingPremium=0.0 component "default.calculator", ICostCalculator,DefaultCostCalculatorRun it one more time: Cost to deliver Order 1: 211.2500 Cost to deliver Order 2: 260.0 So, that's that. Alex has some great comments in his post about other stuff you can do with the Decorator pattern, so I highly recommend you read his all the way through. Next time we'll do this stuff without decorators....