How to Reduce Cyclomatic Complexity Part 14: Service Locator Applied

by Zoran Horvat

In this article we will take a look at one realistic implementation of the Service Locator pattern. This pattern will be applied to the e-commerce application which was refactored throughout this series of articles.

In that application, service methods were returning different kinds of objects that conform to certain interfaces. The problem is that the controller, which receives these objects, is now in trouble to find the view which is appropriate for an object at hand.

One way to resolve views from objects that need to be rendered is to ask for the type of the object:

namespace Store.Presentation.Controllers
{
    public class HomeController
    {
        private readonly IApplicationServices application;

        ...

        public ActionResult Purchase(string itemName)
        {
            IReceiptViewModel receipt = this.application.LoggedInUserPurchase(itemName);
            return SelectView(receipt);
        }

        public ActionResult AnonymousPurchase(string itemName)
        {
            IReceiptViewModel receipt = this.application.AnonymousPurchase(itemName);
            return SelectView(receipt);
        }

        private ActionResult SelectView(IReceiptViewModel receipt)
        {

            if (receipt is ReceiptDto)
                return new SuccessfulPurchaseView(receipt as ReceiptDto);

            if (receipt is ReceiptNullObject)
                return new FailedPurchaseView(receipt as ReceiptNullObject);

            if (receipt is DownForMaintenance)
                return new MaintenanceView(receipt as DownForMaintenance);

            if (receipt is InvalidUser)
                return new InvalidUserView(receipt as InvalidUser);

            if (receipt is OutOfStock)
                return new OutOfStockView(receipt as OutOfStock);

            if (receipt is InsufficientFunds)
                return new InsufficientFundsView(receipt as InsufficientFunds);

            throw new ArgumentException(string.Format("Unsupported receipt view model {0}.",
                                        receipt.GetType().Name));

        }
    }
}

In this piece of code, controller contacts application service to perform a purchase. Application service, in turn, returns an object implementing IReceiptViewModel interface. This resulting object represents the receipt for the purchase. But the controller must return ActionResult. Therefore, we have a problem of mapping an object implementing IReceiptViewModel into another object of a class derived from ActionResult.

We can refactor this code to use a separate dictionary which maps view model objects to appropriate ActionResult objects. This will significantly reduce cyclomatic complexity of the controller, while at the same time not incurring any of the issues normally connected with Service Locator.

Implementing the Service Locator

In this application we will construct a specific class which maps receipt objects to corresponding ActionResult objects.

One thing to notice about this particular problem is that we have to construct new ActionResult every time we need it. Take SuccessfulPurchaseView as an example:

namespace Store.Presentation.Views
{
    public class SuccessfulPurchaseView: ActionResult
    {
        private readonly ReceiptDto receipt;

        public SuccessfulPurchaseView(ReceiptDto receipt)
        {
            this.receipt = receipt;
        }

        public override void Render()
        {
            Console.WriteLine("Dear {0}, thank you for buying {1} for ${2:0.00}",
                                this.receipt.Buyer, this.receipt.ItemName,
                                this.receipt.Price);
        }
    }
}

This view derives from ActionResult and it requires a receipt as its constructor parameter. We can make this idea universal - all ActionResults we are interested in will require an object implementing IReceiptViewModel interface. For any concrete type of the receipt there will be exactly one ActionResult which handles it. Connecting the two types will actually be the task of the Service Locator implementation.

Code tells more than words. Here is the complete Service Locator implementation:

namespace Store.Presentation.ViewLocators
{
    public class ReceiptViewLocator
    {

        private IDictionary<Type, ConstructorInfo> receiptTypeToActionResultCtor =
            new Dictionary<Type, ConstructorInfo>();

        public void Register(Type actionResultType)
        {

            if (actionResultType == null)
                throw new ArgumentNullException("actionResultType");

            if (!typeof(ActionResult).IsAssignableFrom(actionResultType))
                throw new ArgumentException("Type must derive from ActionResult");

            ConstructorInfo ctor = this.FindConstructor(actionResultType);
            Type parameterType = ctor.GetParameters()[0].ParameterType;

            this.receiptTypeToActionResultCtor.Add(parameterType, ctor);

        }

        public ActionResult Locate(IReceiptViewModel receipt)
        {

            if (receipt == null)
                throw new ArgumentNullException("receipt");

            ConstructorInfo ctor = this.FindConstructor(receipt);

            return (ActionResult)ctor.Invoke(new object[] { receipt });

        }

        private ConstructorInfo FindConstructor(Type actionResultType)
        {

            Option<ConstructorInfo> ctor =
                actionResultType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
                .Where(c =>
                    c.GetParameters().Length == 1 &&
                    typeof(IReceiptViewModel).IsAssignableFrom(c.GetParameters()[0].ParameterType))
                .AsOption();

            if (!ctor.Any())
                throw new ArgumentException("Appropriate constructor not found.");

            return ctor.Single();

        }

        private ConstructorInfo FindConstructor(IReceiptViewModel receipt)
        {

            Type receiptType = receipt.GetType();
            Option<ConstructorInfo> ctor = this.receiptTypeToActionResultCtor.TryGetValue(receiptType);

            if (!ctor.Any())
                throw new ArgumentException(string.Format("Receipt type {0} not registered.",
                                                          receiptType));

            return ctor.Single();

        }
    }
}

This class basically maps receipt view models to constructors that can be used to create ActionResult objects. Internally, locator maintains a dictionary mapping the type to the desired constructor.

Public interface of the locator class consists of two methods. One method is Register, and it just receives the type derived from ActionResult. This type must have a public constructor which only receives one argument, and that argument must be an object implementing IReceiptViewModel. If all these conditions are satisfied, the service locator will be able to construct ActionResult when it receives a receipt view model.

The second public method is Locate, and it receives an object implementing IReceiptViewModel. Service locator then looks into its private dictionary and finds the appropriate constructor. This constructor will create a new object derived from ActionResult. That is the whole mechanism used by the Service Locator to locate ActionResult from a receipt.

By the way, note that the ReceiptViewLocator class uses Option<T> functional type, which we have added to the project earlier. See How to Reduce Cyclomatic Complexity: Option<T> Functional Type for details. Also, there are two extension methods used in this code. One is AsOption extension method on IEnumerable. The other is TryGetValue extension method on IDictionary. Both extension methods return Option.

Using the Service Locator

Controller class used to resolve views manually, by testing the type of the receipt that needs to be presented in the view. With service locator available, this operation becomes much easier to implement:

namespace Store.Presentation.Controllers
{
    public class HomeController
    {
        private readonly IApplicationServices application;
        private readonly ReceiptViewLocator receiptViewLocator;

        public HomeController(IApplicationServices application,
                              ReceiptViewLocator receiptViewLocator)
        {
            this.application = application;
            this.receiptViewLocator = receiptViewLocator;
        }

        ...

        public ActionResult Purchase(string itemName)
        {
            IReceiptViewModel receipt = this.application.LoggedInUserPurchase(itemName);
            return SelectView(receipt);
        }

        public ActionResult AnonymousPurchase(string itemName)
        {
            IReceiptViewModel receipt = this.application.AnonymousPurchase(itemName);
            return SelectView(receipt);
        }

        private ActionResult SelectView(IReceiptViewModel receipt)
        {
            return this.receiptViewLocator.Locate(receipt);
        }
    }
}

First change to the class implementation is that the controller now expects the service location through its constructor. The second change is that SelectView method now simply delegates the call to the service locator.

Configuring the Service Locator

In order to make the whole system work, we have to configure the Service Locator. This means that all target types must be registered with it. Here is the code which does precisely that:

namespace Store.Presentation
{

    public class Program
    {

        static HomeController CreateController()
        {
            return
                new HomeController(
                    new ApplicationServices(
                        new DomainServices(
                            new UserRepository(),
                            new ProductRepository(),
                            new AccountRepository())),
                    CreateReceiptViewLocator());
        }

        static ReceiptViewLocator CreateReceiptViewLocator()
        {

            ReceiptViewLocator locator = new ReceiptViewLocator();

            locator.Register(typeof(SuccessfulPurchaseView));
            locator.Register(typeof(FailedPurchaseView));
            locator.Register(typeof(MaintenanceView));
            locator.Register(typeof(InvalidUserView));
            locator.Register(typeof(OutOfStockView));
            locator.Register(typeof(InsufficientFundsView));

            return locator;

        }

        ...

    }
}

Configuration now includes the part in which all supported views are added to the Service Locator. This locator object is then passed to the HomeController when it is constructed. After this point, the whole tree of objects is created and application is ready to run.

Conclusion

In this article we have seen one realistic application of the Service Locator design pattern. Although generally cited as an anti-pattern, this pattern is in fact quite applicable in some cases.

When code is not object-oriented by its nature, such as presentation code is, and it relies on certain mapping between objects and their related information, then we can apply the Service Locator pattern and significantly simplify the code. Benefits of Service Locator, when used in such settings, is that mapping becomes a matter of configuration.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub