Quick Review on the Strategy Pattern…!

So you have a set of similar classes where you want to encapsulate a family of related algorithms. Each the classes have different variations of a similar algorithm or business rules and they need a way to evolve separately from the classes that are using (consuming) them. In this fictitious example we are a phone retailer, could be a system we use online as well as the POS system that is used at an actual brick and mortar store. Now when selling phones you have to order phones from different distributors or manufactures, each manufacture having different business rules and services you may need to call in order to for the manufacture’s distribution center to fulfill your order. The Strategy Pattern comes in handy here so that we can effectively manage these different types of custom implementations for ordering from all these different types of manufactures while not having to rebuild, or compile our OrderService every time we start doing business with new phone manufacture. Last but not least it makes managing our code with all these different manufactures a whole heck of a lot easier…!

So in this example our manufactures will be Apple, HTC, Nokia, and Sony. Each will have their own processes and business rules to adhere to when placing order and getting your phone order fulfilled for your customers.

Our domain model (pretty self-explanatory, so I won’t will dive into any details on this)

1-30-2012 6-07-40 PM

Let’s take a look at what are implementation would like in order to handle all the different manufactures in our domain before implementing the Strategy Pattern.

So we have our OrderService, a class that handles all the heavy lifting for ordering from any given manufacture we do business with. Each case in our switch statement having custom implementation for that given manufacture, I’ve kept this pretty simple to get the point across however in the real world you could probably expect to do specific validation, business rules, executing different back-end services, etc… for a given manufacture.

VSProject: BeforeStrategyPattern, Class: OrderService.cs

      

    public class OrderService
    {
        public ManufactureInvoice OrderProduct(Product product)
        {
            switch (product.Manufacture)
            {
                case Manufacture.Sony:
                    // call Sony webservices, etc...
                    return new ManufactureInvoice
                    {
                        Cost = product.Cost,
                        EstimatedArrivalDate = DateTime.Now.AddDays(5),
                        EstimatedShippedDate = DateTime.Now,
                        Tax = product.Cost * .07M,
                        Manufacture = Manufacture.Sony
                    };

                case Manufacture.Apple:
                    // call Apple webservices, etc...
                    return new ManufactureInvoice
                    {
                        Cost = product.Cost,
                        EstimatedArrivalDate = DateTime.Now.AddDays(5),
                        EstimatedShippedDate = DateTime.Now,
                        Tax = product.Cost * .07M,
                        Manufacture = Manufacture.Apple
                    };

                case Manufacture.Nokia:
                    // call Nokia webservices, etc...
                    return new ManufactureInvoice
                    {
                        Cost = product.Cost,
                        EstimatedArrivalDate = DateTime.Now.AddDays(5),
                        EstimatedShippedDate = DateTime.Now,
                        Tax = product.Cost * .07M,
                        Manufacture = Manufacture.Nokia
                    };

                case Manufacture.Htc:
                    // call Htc webservices
                    return new ManufactureInvoice
                    {
                        Cost = product.Cost,
                        EstimatedArrivalDate = DateTime.Now.AddDays(5),
                        EstimatedShippedDate = DateTime.Now,
                        Tax = product.Cost * .07M,
                        Manufacture = Manufacture.Htc
                    };

                default:
                    throw new UnknownManufactureException();
            }
        }
    }


So just by looking at this class we can already see that if we were to making any changes to any of the implementation for any of these distributors we would have to making changes to this class because of the tight coupling of the OrderService and our ordering algorithms for each individual manufactures.

Let’s see what the Strategy Pattern can do for us, first we are going to add a few an interface and a few class which will implement this interface (VS Project: AfterStrategyPatternA)

Each of the classes that implement the IManufactureOrderStrategy interface which will be an ordering strategy for that manufacture, where you could call different services, business rules, validation, etc… They all related, in which they all order a phone from a manufacture and fulfill our order, however the only difference is they way they may behave for a given manufacture. Now we will make changes to our OrderService class where our clunky switch statement was earlier

VS Project: AfterStrategyPatternA, Class: OrderService.cs


    class OrderService
    {
        public readonly IOrderingStrategy _manufactureOrderingStrategy;

        public OrderService(IOrderingStrategy manufactureOrderingStrategy)
        {
            _manufactureOrderingStrategy = manufactureOrderingStrategy;
        }

        public ManufactureInvoice OrderProductFromManufacture(Product product)
        {
            return _manufactureOrderingStrategy.OrderProduct(product);
        }
    }

As you can see, instead of the OrderService containing the actual business logic and implementation for ordering with each of our manufactures, we are now passing this into the OrderService class. Let’s take a quick look on how what the consumer code would look
like using our new decoupled OrderingService.

VS Project: AfterStrategyPatternA, Class: OrderProductFromManufacture_Test.cs


    [TestClass]
    public class OrderProductFromManufacture_Test
    {
        [TestMethod]
        public void When_Manufacture_is_From_Apple()
        {
            var _order = MockOrderPipeline.CreateOrder();
            
            var appleProduct = _order.OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Apple)
                .Single()
                .Product;

            var appleOrderStrategy = new AppleOrderStrategy();
            var manufactureOrderingService = new OrderService(appleOrderStrategy);
            var manufactureInvoice = manufactureOrderingService.OrderProductFromManufacture(appleProduct);

            Assert.AreEqual(manufactureInvoice.Manufacture, Manufacture.Apple);
        }

        [TestMethod]
        public void When_Manufacture_is_From_Htc()
        {
            var _order = MockOrderPipeline.CreateOrder();
            var htcProductFromOrder = _order.OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Htc).Single().Product;

            var htcOrderStrategy = new HtcOrderStrategy();
            var manufactureOrderingService = new OrderService(htcOrderStrategy);
            var manufactureInvoice = manufactureOrderingService.OrderProductFromManufacture(htcProductFromOrder);

            Assert.AreEqual(manufactureInvoice.Manufacture, Manufacture.Htc);
        }

        [TestMethod]
        public void When_Manufacture_is_From_Nokia()
        {
            var _order = MockOrderPipeline.CreateOrder();
            var nokiaProductFromOrder = _order.OrderDetails.Where(o => o.Product.Manufacture == Manufacture.Nokia).Single().Product;

            var nokiaOrderStrategy = new NokiaOrderStrategy();
            var manufactureOrderingService = new OrderService(nokiaOrderStrategy);
            var manufactureInvoice = manufactureOrderingService.OrderProductFromManufacture(nokiaProductFromOrder);

            Assert.AreEqual(manufactureInvoice.Manufacture, Manufacture.Nokia);
        }

        [TestMethod]
        public void When_Manufacture_is_From_Sony()
        {
            var _order = MockOrderPipeline.CreateOrder();
            var sonyProductFromOrder = _order.OrderDetails.Where(o => o.Product.Manufacture == Manufacture.Sony).Single().Product;

            var sonyOrderStrategy = new SonyOrderStrategy();
            var manufactureOrderingService = new OrderService(sonyOrderStrategy);
            var manufactureInvoice = manufactureOrderingService.OrderProductFromManufacture(sonyProductFromOrder);

            Assert.AreEqual(manufactureInvoice.Manufacture, Manufacture.Sony);
        }
    }

Note that for each of these calls we are passing a custom implementation (e.g. AppleOrderStrategy, HtcOrderStrategy, NokiaOrderStrategy, SonyOrderStrategy) of for each of the manufactures who all implement the IOrderingStategy interface. So now our OrderingService class adheres to the “open close principle”, where our OrderingService should be open for extension, but closed for modification.

Finally, let’s take a look a different form of using the Strategy Pattern using Funcs, delegates and lambdas (VS Project: AfterStrategyPatternB, class: OrderProductFromManufacture_Test).

First let’s take a quick look at our enhanced OrderingService.

VS Project: AfterStrategyPatternB, Class: OrderService.cs


    public class OrderService
    {
        public ManufactureInvoice OrderProduct(Product product, 
            Func<Product, ManufactureInvoice> orderingStrategy)
        {
            return orderingStrategy(product);
        }
    }

Notice how we are now passing in a generic func vs. a class that, and the func expects the Product to be ordered to be passed into it where it will then return a ManufactureInvoice for the product that was ordered and to be fulfilled.

Now let’s take a the new OrderService consuming code, notice how we can now still pass in our implementation as long as we are compliant with the generic func parameter of our OrderServce.OrderProduct method. We can somewhat think of the generic func as our new contract, contract meaning we have to pass in a Product entity and we have to return a ManufactureInvoice after we have completed the ordering process.

VS Project: AfterStrategyPatternB, Class: OrderProductFromManufacture_Test.cs


    [TestClass]
    public class OrderProductFromManufacture_Test
    {        
        [TestMethod]
        public void When_Manufacture_is_Apple()
        {
            // using lambda expression
            Func<Product, ManufactureInvoice> appleStrategy = product => 
                new ManufactureInvoice { 
                    Cost = product.Cost, 
                    EstimatedArrivalDate = DateTime.Now.AddDays(5), 
                    EstimatedShippedDate = DateTime.Now, 
                    Tax = product.Cost * .07M, 
                    Manufacture = Manufacture.Apple };

            var _order = MockOrderPipeline.CreateOrder();

            var appleProduct = _order.OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Apple)
                .Single()
                .Product;

            OrderService orderService = new OrderService();
            orderService.OrderProduct(appleProduct, appleStrategy);
        }

        [TestMethod]
        public void When_Manufacture_is_Sony()
        {
            // using delegate
            Func<Product, ManufactureInvoice> sonyStrategy = 
                delegate(Product product) { return new ManufactureInvoice { 
                    Cost = product.Cost, 
                    EstimatedArrivalDate = DateTime.Now.AddDays(5), 
                    EstimatedShippedDate = DateTime.Now, 
                    Tax = product.Cost * .07M, 
                    Manufacture = Manufacture.Sony }; };

            var _order = MockOrderPipeline.CreateOrder();

            var sonyProduct = _order.OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Sony)
                .Single()
                .Product;

            OrderService orderService = new OrderService();
            orderService.OrderProduct(sonyProduct, sonyStrategy);
        }

        [TestMethod]
        public void When_Manufacture_is_Nokia()
        {            
            var _order = MockOrderPipeline.CreateOrder();
            
            // using inline lambda expression (fluent)
            var sonyProduct = _order
                .OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Sony)
                .Single()
                .Product;

            OrderService orderService = new OrderService();

            orderService.OrderProduct(sonyProduct, product =>
            {
                return new ManufactureInvoice { 
                    Cost = product.Cost, 
                    EstimatedArrivalDate = DateTime.Now.AddDays(5), 
                    EstimatedShippedDate = DateTime.Now, 
                    Tax = product.Cost * .07M, 
                    Manufacture = Manufacture.Sony };
            });
        }

        [TestMethod]
        public void When_Manufacture_is_Htc()
        {
            var _order = MockOrderPipeline.CreateOrder();
            
            // using anonymous method
            var htcProduct = _order
                .OrderDetails
                .Where(o => o.Product.Manufacture == Manufacture.Htc)
                .Single()
                .Product;

            OrderService orderService = new OrderService();
            orderService.OrderProduct(htcProduct, OrderProductFromHtc);
        }

        private ManufactureInvoice OrderProductFromHtc(Product product)
        {
            // e.g. invoke htc webservices, invoke order services, invoke dropshipment services, publish events to service
            return new ManufactureInvoice { 
                Cost = product.Cost, 
                EstimatedArrivalDate = DateTime.Now.AddDays(5), 
                EstimatedShippedDate = DateTime.Now, 
                Tax = product.Cost * .07M, 
                Manufacture = Manufacture.Htc };
        }
    }


Download the Strategy Pattern sample VS Solution: https://skydrive.live.com/embed?cid=949A1C97C2A17906&resid=949A1C97C2A17906%21369&authkey=AD0Eu1b0USnB5do