The primary purpose of a Service Locator IMO is to act as Registry of sorts to obtain managed objects. The Service Locator can also be augmented with functionality to lazily load the serviced object and also provide as a single place where a new copy of the serviced object can be obtained (think prototype pattern here).
One simple example of obtain an object using the Service locator is shown below:
public class ServiceLocator { private static final ServiceLocator instance = new ServiceLocator(); private ServiceLocator() {} public static ServiceLocator getInstance() { return instance; } private OrderDAO dao = new OrderDAOImpl(); public OrderDAO getOrderDAO() { return dao; } } public class OrderServiceImpl { private final OrderDAO orderDAO; public OrderServiceImpl() { orderDAO = ServiceLocator.getInstance().getOrderDAO(); } }
In the above example, the OrderDAO implementation is obtained from the Service Locator via a strongly typed call to getOrderDAO(). This is quite nice. However, what if we had many different objects that we would like to locate? Adding multiple methods like getProductDAO(), getCartDAO(), getAuditService()...etc will tend to bloat the ServiceLocator with a lot of code. One could create separate Service Locator classes for example, ProductDAOLocator, CreditCardDAOLocator etc, etc. However, we again have a lot of classes to maintain and thus a penalty to pay.
What if we could provide an easy way to obtain the objects via a "key"? This method involves using a "key" into the Service Locator based of which the located examines its registry and provides back the corresponding object. Consider an altered version of the locator as shown below:
public class ServiceLocator { private static final ServiceLocator instance = new ServiceLocator(); private Map locatorMap = new HashMap(); private ServiceLocator() { locatorMap.put("orderDAO", new OrderDAOImpl()); locatorMap.put("productDAO", new ProductDAOImpl()); locatorMap.put("auditService", new AuditServiceImpl()); } public static ServiceLocator getInstance() { return instance; } public void setServicedObject(String key, Object impl) { locatorMap.put(key, impl); } public Object getServicedObject(String key) { return locatorMap.get(key); } } public class OrderServiceImpl { private final OrderDAO orderDAO; public OrderServiceImpl() { orderDAO = (OrderDAO) ServiceLocator.getInstance() .getServicedObject("orderDAO"); } } public class OrderServiceTest { public void setUp() { ServiceLocator.getInstance().setServicedObject("orderDAO", new MockOrderDAOImpl()); } public void testService() { .... } }In the above example, the code is more dynamic and it becomes easy to add more locatable services. In addition, the above locator works very well when using a command/factory pattern as shown below:
public class ServiceLocator { private static final ServiceLocator instance = new ServiceLocator(); private Map locatorMap = new HashMap(); private ServiceLocator() { locatorMap.put("saveOrder", new OrderSaveCommand()); locatorMap.put("saveProduct", new ProductSaveCommand()); ..... } public static ServiceLocator getInstance() { return instance; } public void setServicedObject(String key, Object impl) { locatorMap.put(key, impl); } public Object getServicedObject(String key) { return locatorMap.get(key); } } public class Action { public void doAction(Request request) { String commandKey = request.getCommandKey(); CommmandIF c = (CommandIF) ServiceLocator.getInstance() .getServicedObject(commandKey); c.executeCommand(request); } }
The ServiceLocator functions as a factory of sorts here providing concrete implementations of the Command interface at runtime based of the key.
The above examples of Service Locator do have a major short coming. The code is now not strongly typed. Compile time type checking is lost. In the example which explicitly maintained getter methods for each known service, a call to a getter guranteed compile time type checking and run time safety. In the above shown versions of the Locator, if the Registry had an accidentally mapping of ["orderDAO", new ProductDAOImpl()], at run time a ClassCastException can occur.
Clearly both the styles of using the Service Locator pattern shown above have their pro's and con's. Can we think of a third way that will provide compile time type checking of the first example while providing the ease of use of the second?
Yes, we have hope with Java 5.X+ and Generics. With Generics introduced with Java 5.X, compile time type safety has obtained a big boost. In order to explore the problem further, I must credit the work influenced by:
- Generic Service Locator - A .NET implementation I guess.
- Registry example from Rafael's blog
- Martin Fowlers take on the ServiceLocator/DI patterns.
- A colleague/genius with whom I have had the pleasure to work with.
With the above examples, we have laid down the pattern for implementing a type safe version of the Service Locator. The key to lookup a class is now the interface. With the same said, the following snippet represents a new type safe version of the Service Locator:
public class ServiceLocator { private static Map<Class<?>, Object> infInstanceMap = new HashMap<Class<?>, Object>(); public static <T, I extends T> void setService(Class<T> inter, I impl) { infInstanceMap.put(inter, impl); } public static <T> T getService(Class<T> ofClass) { Object instance = infInstanceMap.get(ofClass); return ofClass.cast(instance); } } public class BootStrap { public static void start() { ServiceLocator.setService(OrderDAO.class, new OrderDAOImpl()); // Note the following will not compile // ServiceLocator.setService(ProductDAO.class, new OrderDAOImpl()); } } public class OrderService { private final OrderDAO orderDAO; public OrderService() { orderDAO = ServiceLocator.getService(OrderDAO.class); } } public class OrderServiceTest { public void setUp() { ServiceLocator.setService(OrderDAO.class, new MockOrderDAOImpl()); } public void testService() { ... } }The above example is type safe while promoting testability. So is there some functionality we have lost with this locator? The answer is 'yes'. The locator does not permit the use of multiple implementations of the same interface for location. Two values for the same key becomes a bit of a problem. How can we overcome the same?
Well, I do not know if this is good solution or not. However, what I propose is that if there are more than one implementations of a given interface that need to be located, then the implementors need to specifically identify themselves via unique identifiers (Think Spring annotations and @Qualifier here). So the solution is, if there is exactly one known implementation, then use the interface based lookup, if not and this should be known during development, lookup by a unique identifier. Applying these changes we have:
public class ServiceLocator { // Code for interface based lookup is not show here but should be considered included ....... ....... private static Map<String, Object> idObjectMap = new HashMap<String, Object>(); public static <T, I extends T> void setIdBasedLookupService(String uid, Class<T> infClass, I impl) { idObjectMap.put(uid, impl); } public static <T> T getServiceById(String uid, Class<T> infClass) { Object instance = idObjectMap.get(uid); return ifClass.cast(instance); } } public class BootStrap { public static void start() { ServiceLocator.setIdBasedLookupService("saveOrderCommand", CommandIF.class, new SaveOrderCommand()); // Note the following will not compile // ServiceLocator.setService("saveProductCommand", CommandIF.class, // new OrderDAOImpl()); } } public class Action { public execute(Request request) { String commandKey = request.getCommandKey(); CommandIF command = ServiceLocator .getServiceById(commandKey, CommandIF.class) command.execute(); } } public class RequestTester { public void setUp() { ServiceLocator.setIdBasedLookupService("saveOrderCommand", CommandIF.class,new MockSaveCommand()); } public void testRequest() { ... } }
Ok so are we done yet? I hear the shouts of "About time!" but beg for forgiveness and continue :-)
There are some things that I do not like about the above approach:
- Explicit setting of the objects in the locator by the BootStrap. Every locatable interface and implementation needs to be specified.
- No lazy loading option
I would like to lean on my favorite open source framework, Spring's classpath scanning feature of locating interfaces and implementations. In addition, Spring's feature of using the @Qualifier tag to denote the case where there are multiple implementations.
To facilitate the same, I define an Annotation called @Locatable which interfaces that will participate in the Service Location process will need to employ. If an interface has only one concrete implementation, then the same will be dicovered and bound in the locator. If there are multiple implementations, then those implementations must have an annotation called @ImplUid that contain's one attribute called uid. So as examples of the same, see below:
@Locatable public interface OrderService { public void saveOrder(Order order); } public abstract class AbstractOrderServiceImpl implements OrderService { .... } // Single Concrete OrderService implementation public class OrderServiceImpl extends AbstractOrderServiceImpl { public void saveOrder(Order order) { .... } } @Locatable public interface Phone { public boolean dialNumber(int number); } // Analog implementation of Phone @ImplUid(uid="analogPhone") public class AnalogPhone implements Phone { public boolean dialNumber(int number) { .... } } // Digital implementation of phone. @ImplUid(uid="digitalPhone") public class DigitialPhone implements Phone { public boolean dialNumber(int number) { ... } }In the above example, note that for the OrderService there is only one concrete implementation, namely, the OrderServiceImpl. However, for the Phone interface, there are two implementation and each are tagged with the @ImplUid annotation to set them apart.
In order to perform the tie up between interface and implementation, I have defined a class called LocatorClassFinder. This class will scan the classpath, perform the tie up between interfaces and implementations and then return back two maps. The first map will map [interface name to class name] for cases where there is only one implementation of a given interface. The second map provides mapping for interfaces that have multiple implementation and the implementations are correspondingly tagged with @ImplUid. The ServiceLocator has a start() method that will ask the LocaterClassFinder to construct the maps. The actual class path scanning is performed using Spring frameworks adapters of the asm library. When the Service locater is asked for an object, it will lazily instantiate the same based of the information provided by the two maps. So summarizing via code:
public class ServiceLocator { .... // All look up/setter static methods discussed earlier. private IntefaceImplMaps maps; // Called once at startup public static void start() { // Package to scan LocatorClassFinder finder = new LocatorClassFinder("com/welflex"); maps = finder.locateInterfaceImpls(); } } public class LocatorClassFinder { ..... public InterfaceImplMaps locateInterfaceImpls() { .... } }With the above solution, we have an easy way to implement Service Location with support for the command/factory patterns as well. There is no explicit mapping of interface and impls required for the developer as these are calculated at run time.
In Summary:
I have an example of the implementation of the Service Locator available HERE for Download. It is a Maven 2 project. The example is purely for purposes of demonstration and learning. I have used code from the Spring project and am NOT redistributing the same anywhere or utilizing it in any project of sorts. It is purely an educational exercise.
- The LocaterClassFinder implemented uses code from Spring Framework. Some clases that the Spring framework has scoped as Package protected, I have simply copied and used. There is no compelling reason to use Spring's asm support. I however found it to be "easy". In addition, some other Spring classes are being used to acheive the classpath scanning. The example of Service Locator does not depend on the Spring container in any way and only uses certain classes provided by Spring. It can easily be stripped of all dependencies to spring if required.
- Although the example code hopes to facilitate lazy loading, issues with synchronicity have NOT been addressed with the same. Should not be hard to do so.
- The code has not discussed creating objects via the Prototype pattern. However, that can easily be done with some tweaking
- The LocaterClassFinder class is a very hacky and bad implementation. It was done just to test the theory and is by no means ready to stand the rigors of a code review :-)) I am ashamed of the same and blame it on a 3 drinks on a saturday night ;-)
1 comment:
Thanks for the great examples. That is exactly I am looking for.
Post a Comment