A couple of weeks ago, I did some work together with Eric Evans when he came to Uppsala to give his excellent course in domain driven design, which was co-hosted by Citerus and Patrik Fredriksson.
Eric is the project leader of the Time and Money Java library, which makes working with dates, time intervals, currencies and so on a breeze. However, inspired by this article by Guillame Laforge, I wanted to see if I could create something similar by leveraging Groovy and the Time and Money library. These are a few simple examples that I came up with in an hour:
println 1.minute
=> '1 minute'
println 5.minutes + 1.minutes
=> '6 minutes'
println "2003-05-16" + 3.weeks - 50.years
=> 'Sat Jun 06 01:00:00 CET 1953'
Looking at these statements from top to bottom, we first have
1.minute
The number 1 is of course an instance of java.lang.Integer, a full-blown object. On that instance, we access something called minute, which kind of looks like a field, but is actually a JavaBean property thanks to Groovy's built-in support for those. So what we really have is an invocation of Integer.getMinute(), a method that doesn't exist. But don't worry - here's how we can use metaprogramming to add that method to the Integer class:
Integer.metaClass.getProperty = {symbol ->
switch (symbol) {
case ["minute"]:
return Duration.minutes(delegate)
default:
return null
}
}
In Groovy, every class has a corresponding open metaclass, the ExpandoMetaClass, that may be used to dynamically add methods on classes. The method getProperty is invoked when a JavaBean property is accessed, and here we assign a closure to be evaluated on invocation. The closure recieves one argument, symbol, which is a String containing the name of the property accessed, in this case "minute". This particular case is chosen to be converted to a Time and Money datatype, Duration, by passing the delegate (that's the instance we're invoking the getter on, i.e. 1) to the appropriate factory method. The result is that a Duration instance is returned, representing one minute.
Moving on to the next one, we have
5.minutes + 1.minute
The terms being added are familiar by now, although we need to expand the previous closure to this:
Integer.metaClass.getProperty = {symbol ->
switch (symbol) {
case ["minute"]:
return Duration.minutes(delegate)
case ["years", "quarters", "months", "weeks", "days", "hours", "minutes", "seconds"]:
return Duration.getMethod(symbol, int).invoke(null, delegate)
default:
return null
}
}
Fortunately, the Time and Money library has nicely named methods that correspond exactly to how we want to express durations in this DSL, so we can be very efficient and use reflection invocation of factory methods. Oh, and it's very nice to be able to switch on lists, isn't it? :-)
But there's one more thing to it: the overloading of the + operator. We've already established that both 1.minute and 5.minutes are instances of Duration, and luckily the Duration class already has a plus(Duration) method on it, that Groovy will automatically evalute. It's not always the case that there is such a method available though, as we find out when we move on to the third case:
"2003-05-16" + 3.weeks - 50.years
Evaluating from left to right, we're initially adding a String and a Duration, but String does not have any plus(Duration) method, so we're going to have to add that:
String.metaClass.plus = {Duration duration ->
return duration.addedTo(TimePoint.parseGMTFrom(delegate, "yyyy-MM-dd"))
}
Here, we're using the Time and Money API to represent the String as a TimePoint, to which a Duration may be added, producing another TimePoint. Continuing our evaluation, we now have to subtract a Duration from a TimePoint, which should be quite familiar by now:
TimePoint.metaClass.minus = {Duration duration ->
return duration.subtractedFrom(delegate)
}
These are just a few examples, and the possibilities are vast.