Example of Simplicity in Design
It’s all well and good to preach simplicity, but if you don’t provide practical examples then no-one can get it. I’m going to highlight one section of Java ActionPack to show simplicity in action. The first mistake that people make when talking about simplicity is confusing it with simplisticness.
Simplicity is the ultimate sophistication. Leonardo DaVinci
So what does it mean to use simplicity in code? First, simplicity has to do with keeping the responsibilities of your code small and focused . You should resist adding responsibilities to a bit of code unless there is no better place to put it. The org.dhaven.actionpack.Route class in Java ActionPack takes a URL pattern, and set of default values and populates request attributes with the substitution values. That’s it. Nothing else. It’s the controller code that reacts to the special attributes “controller” and “action” to find the controller and action to execute. The org.dhaven.actionpack.Routes class will allow you to register a series of Route objects to evaluate in order. It also provides a method to iterate through the Routes and return when the first match has been found. Pretty sweet, huh? Two classes that have a relationship, but their responsibilities are very small.
Nice overview, but can we look at things in more detail? Sure thing. I wanted to make sure it was easy to programatically set up the Routes you wanted your application to use. If you didn’t want a routes file hanging around, or you wanted to embed this framework someplace I hadn’t expected, it shouldn’t be hard to do. I.e., I shouldn’t have to try and set up an object tree like the Apache Digester based projects. When you try to start from the configuration file format, instead of how you should add new routes to the system it gets really ugly really fast. So I wanted to make it simple , or easy to do. My solution? Why not simply call Routes.connect() ? If you were to set up the routes manually, it would look like this:
public static void setUpMyRoutes() {
Routes.connect("/{controller}/{action}/{id}");
Routes.connect("/{controller}/{action}");
Map<String,String> initParams = new HashMap<String,String>();
initParams.put("controller", "home");
initParams.put("action", "index");
Routes.connect("/{controller}", initParams);
Routes.connect("", initParams);
}
Notice that I’m only passing strings into the Routes object? In turn, the Routes object doesn’t do anything with these strings other than creating Route objects with the parameters passed in to the constructor. It’s the Route object itself that knows what to do with the provided strings. The initParams map is also a simple map, which provides default values. The default values are overridden by the values interpreted by the URL. What I haven’t done, and I probably ought to do is to make it so that if defaults are present, the same Route can match partial URLs like the Ruby on Rails version of the functionality can. Perhaps that is a future enhancement. Remember, it is only the Route object I have to alter to make that possible. Setting up the Routes is as simple as it gets.
So what if I don’t want to worry about setting up the routing myself? Well, why not create a route parser? That’s exactly what we did. The org.dhaven.actionpack.RouteMapParser handles parsing a very simple route file format that isn’t XML. In fact, it’s similar to the Ruby format (with minor differences). I didn’t want a full on Ruby parser, just a convenient format. If you call org.dhaven.actionpack.RouteMapParser.parseFile() while passing in an InputStream or Reader, you can have it parse the file for you. The parser is very simple as well. First, all routes are completely defined on one line. Second, blank lines and lines that start with the ’#’ character are ignored. Why the ’#’ character? Because it is used as the comment marker in Properties files, shell scripts, and Ruby files. It should be reasonable to assume people know that is a comment. So what does a line look like? Well, let’s do the same thing with our file that we did in the method above.
"/{controller}/{action}/{id}"
"/{controller}/{action}"
"/{controller}", action => "index"
"", controller => "home", action => "index"
The first part of the line is the URL in quotes. This is the “route” that is being connected through the Routes class. All elements of a route are separated by a comma. That means the route and all default request attributes are separated by commas. The key/value of each map entry is separated by the ’=>’ combination. That separator is a convention used in Ruby and Javascript, but it looks right. No quotes for the key, and the element is surrounded by quotes. There really isn’t much of a reason for this convention other than I thought it looked easy on the eyes. With everything in quotes it got tough to keep straight what were the keys and what were the values. Now, all the key/value pairs are strings. It’s up to you to perform the conversions to other types in the controller if you want. The reason for that has to do with simplicity of design, and not getting the whole route map process too convoluted.
The RouteMapParser takes care of calling the Routes.connect() method which in turn takes care of creating the Route and putting it in our list of routes to match against. Each responsibility is clearly mapped to one class. If someone else wanted a different format for setting this up, there would be nothing to stop them from doing it. It also makes understanding each class much easier. The RouteMapParser only has the responsibility of converting a file to a set of routes. The Routes object only has the responsibility of managing the list of routes and testing a request against that list. The Route object has the responsibility of trying to match the URL, and then populating the attributes if there is a match. The Route object may be expanded in the future to take the parameters and generate a URL from them (as Ruby On Rails currently does), but that is a future enhancement.
The bottom line is that the responsibilities are finite and related to each other. More importantly, the concerns of configuring the system are kept separate from the code that does the work. The number of objects needed to do simple things is very small, and the interactions are well defined and easy to understand. The simplicity allows the objects to be used in ways I may have not foreseen, but that doesn’t really matter. What matters is that the sophisticated uses I haven’t foreseen are because of the simplicity of the classes governing that responsibility.
