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.
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.
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.
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.
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
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
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.