Thursday, August 16, 2007

A Timer for Everything (A TimerCategory)

While writing the chapter about the GDK, I found that the support for the Timer was less than perfect.

First Question: What is a Timer? A timer is an object that contains a thread of its own that works on TimerTask instances, calling them either once or repeatedly. To create a TimerTask object you derive a new class that implements the run() method, create an object and you are ready to start this. The Timer class offers multiple methods, that start a timer task once after a delay, after a delay and then repeatedly (either fixed rate or interval-based), using Date instances or longs as arguments.

While there is a method added to Timer that starts a closure once after a given delay (the method name is runAfter()), there is no such method for repeated execution.

My first idea was to add the missing functionality simply by adding methods to the GDK, which I gaily proposed to do on the Groovy mailing list.

The answer (I'm paraphrasing): The methods are too clumsy. Something DSL-like would be much better e.g.,
Thread.start(in: 20.minutes, every: 1.hours) { ... }

Point taken. This is much more elegant, simpler to use and most probably better to extend.

Second Question: How to implement this? It is quite simple actually. We begin by stealing (another term is reusing). Groovy already has a TimeCategory in org.codehaus.groovy.runtime, that allows things like
20.seconds + 3.minutes.from.now
Pretty cool stuff. See Groovy Dates and Times for details.

Now, instead of attaching the new functionality to Thread we will add it to Timer objects. This way the user still has the option to decide to which Timer object the resulting task will be attached. Furthermore, we do not have to keep some global timer instance that is used to start the different tasks.

We need the following keywords:
  • at
    value should be Date, long or a duration (from the TimeCategory)
  • in (alias after)
    values should be Date, long or a duration
  • every
    values should be long or a duration
  • fixed
    true or false (fixed rate or interval-based rate)

With these you can do all of the following:

t.run(at: new Date(), every: 3.seconds, fixed:true) { ... }
t.run(at: 3.minutes.from.now) { ... }
t.run(in: 5.seconds, every: 3.seconds, fixed:true) { ... }
t.run(after: 5.seconds, every: 3.seconds, fixed:true) { ... }
t.run(after: 5000, every: 3.seconds) { ... }

We need, of course, both the TimeCategory and the TimerCategory to do this:

use(TimeCategory, TimerCategory) {
t = new Timer()

t.run( ... ) {
do the stuff
}

Fine, now for the realization.

To implement a category we need a class that has static methods. The first parameter of a static method denotes the type to which the method will be attached at runtime. So, if I use the Timer class as the first parameter, I later can call the method on a Timer object. In the method I have access to the object on which the method was called (it is the first argument).

The method has two real parameters, a map that contains values for the keys at, in, every and fixed, and a closure. Thus it looks like the following (simplified):

class TimerCategory {
static TimerTask run(Timer timer, Map vals, Closure c) {
task = new ClosureTimerTask(c)
if vals.fixed
get val and set fixed
if vals.after or if vals.after or vals.at
get value and set delay
if vals.every
get value and set period
use values to schedule task with timer
return task
}
}

The real implementation of the category has 45 lines, so there's not much to it. The unit test has over a hundred lines ...

So, by implementing a new category in 45 lines instead of added another few methods to the GDK we actually have made Groovy more elegant. Try to do something like this with Java.

This is part of why I love Groovy so much. You not only have fun programming with it, but also when you find a small imperfection. Maybe especially then.

Third Question: When will you implement your next Category?
Happy hunting ...

You can download the code here.

No comments: