The router as a choice between alternate strategies..
Browse through all the non integration pattern books and you would not find a Router mentioned. The Router pattern has been recognized as an excellent way to accomplish Enterprise Application Integration (EAI). But it is my contention that their role in application programming has not been sufficiently emphasized. This post seeks to introduce the reader to this exotic beast and lets them understand some of the amazing things that can be accomplished by keeping this pattern in mind. The Router, IMNSHO is one of the most beautiful patterns. It can transform an application that is monolithic, non modular, non configurable among other bad things into a thing of beauty. So much so, that the application almost becomes magical. Sounds pompous? Yes. But did this grab your attention at least? If it did, read on.. The router and a close relative called Chain are some of the most interesting patterns that I have run into in my course of developing complex applications. We will talk about chains another time but let us jump to the routers now. So what is a router?
A router is a component (typically a class) that connects its consumer to one of multiple output strategies (as enunciated in the strategy design pattern).
Let us illustrate this with a quick example. Let us say that we want to consume implementations of some interface called AccountService which is defined below:
public interface AccountService {
public AccountDetails getAccountDetails(AccountDetailsInput accountDetailsInput);
}
Let us say we have two implementations of the AccountService interface called DBAccountService and OnlineAccountService. DBAccountService fetches account information from a database (based on yesterday's feed to the database) and OnlineAccountService makes a web service call to obtain this information from the actual source. OnlineAccountService is an expensive call and hence is only used when the usage warrants it. Both these implementations would serve as strategies. Now a typical consumer who wants to use both these strategies selectively may have the following code:
AccountService dbAccountService = ... //somehow initialize it ;
AccountService onlineAccountService = ... // somehow initialize it;
if (real time details are desired){
// use onlineAccountService
} else {
// use dbAccountService
}
Hence the consumer code is now containing the logic that makes a selection between the strategies. This is sub-optimal due to various reasons:
The Router pattern depending on the sophistication that is put into it, would obviate some or all of the aforementioned problems. We will take a stab at discussing some of these nuances in the reminder of this post. The first step in solving this problem is to introduce a new class called an AccountServiceRouter which contains the if then condition. The AccountServiceRouter class itself is quite simple to write:
public class AccountServiceRouter implements AccountService {
AccountService dbAccountService = ... //somehow initialize it ;
AccountService onlineAccountService = ... // somehow initialize it;
public AccountDetails getAccountDetails(AccountInput accountInput) {
AccountDetails accountDetails = null;
if (real time details are desired){
accountDetails = onlineAccountService.getAccountDetails(accountInput);
} else {
accountDetails = dbAccountService.getAccountDetails(accountInput);
}
return accountDetails;
}
}
Now the consumer merely uses AccountServiceRouter rather than the individual account services. This solves the replication problem mentioned above but nothing else. It also gives another headache namely that we need to create a third implementation of AccountService which is intrinsically aware of all the account services that are in existence and can route to each one of them. This is indeed a pain and there are other approaches that solve it but we are not going to concentrate on them now. Anyways, this is just a tad better than what we had before. Now let us see if we can refine the solution a little more. First of all we must recognize that Routers can exist for any kind of service. Hence we may have an AccountServiceRouter, a CustomerServiceRouter, an OrderServiceRouter and so on. All these routers will soon have this problem with configuration i.e. all the logic in them is in if-then-else and hence not configurable. Let us see if we can solve this problem in a generic fashion.
A router is also only as complicated as the complexity of its if-then-else clause. The more complex the clause is, the more complex the router itself becomes. We cannot eliminate the complexity of the if then clause itself without sacrificing functionality. But we can see if we can somehow make the outcome more configurable. For this we need a concept called routing string. A routing string as the name suggests, is just a string representation of the actual route that is taken. For every route we take there would be a routing string associated with it. For instance, in the above example we can have two routing strings "realtime" and "delayed". "realtime" maps to the OnlineAccountService and "delayed" maps to the DbAccountService. Both of these are meaningful to the actual router in question. Now why do we need this routing string abstraction? To answer this let us go ahead with another example. We will create a new interface called OrderService with two associated implementations and a router. So we would have OnlineOrderService, DbOrderService and OrderServiceRouter. The OrderServiceRouter also has similar code as the one above for AccountServiceRouter but the strategies routed to are instances of OnlineOrderService and DbOrderService. This is all great. But if we go with the routing string concept we suddenly realize that the routing strings in this case too would be the same namely "realtime" and "delayed". So we are able to use the same routing string concept across multiple routers in vastly different scenarios. This in itself is a very important realization. But there is more and we will realize it as we start making a more concrete example. Let us go and refactor the AccountServiceRouter above to use the notion of routing string.
public class AccountServiceRouter implements AccountService {
AccountService dbAccountService = ... //somehow initialize it ;
AccountService onlineAccountService = ... // somehow initialize it;
Map<String,AccountService> mapOfServices = new HashMap<String,AccountService>();
public AccountServiceRouter() {
// initialize the mapOfServices.
mapOfServices.put("realtime",onlineAccountService);
mapOfServices.put("delayed",dbAccountService);
}
public AccountDetails getAccountDetails(AccountInput accountInput) {
String routingString = getRoutingString(accountInput);
return mapOfService.get(routingString).getAccountDetails(accountInput);
}
public String getRoutingString(AccountInput accountInput){
if (real time details are desired){
return "realtime";
} else {
return "delayed";
}
}
}
As we see, the getRoutingString() method has been introduced now to compute the routing string. The router maps it to the actual service that needs to be invoked and invokes the service. The functionality is exactly the same as before. But we gained an important thing in the process. Now, if we are able to expose the mapOfServices to a configuration panel then it is possible that the user would have the ability to change the contents of the map in well controlled ways. For instance, the user can map “realtime” to the same service as “delayed” temporarily. This would allow the users to change the behavior of a running application! Suddenly all queries which are diagnosed as “realtime” will get deflected to the same service as all queries diagnosed as “delayed”. This is what I meant when I mentioned in the beginning about the application performing magic. Stuff that previously was believed to be difficult to change, can now be changed dynamically without adding any complexity to the application.
The introduction of routing string paves the way towards dynamic discovery. If it is possible to compute different routing strings under different circumstances, then the classes that implement these routing strings can be dynamically discovered. A detailed analysis of this would be beyond the scope of this post. But interested readers can ask me for some possible approaches.
I would also talk about strategies to unify routers with a Router super class, dynamic implementations of routers for different interfaces (instead of hand coding each router for each interface) etc. in later posts.
Gaurav Malhotra, 2009-01-23 20:25:45
"I totally agree with the concepts of this article."