Wednesday, December 31, 2008

Factoring out helper classes

The job is to get a source and a destination path for a java compile. Because it's mostly the same, we want defaults for source and class directory. Because we occasionally have more than one separate hierarchy to compile, we want to provide a base path on which the defaults are applied. Thus this is the wet (aka non-dry) code:

AttrList al = new AttrList (l.getParam ());

String base = al.pull ("dir");
if (base == null) base = "";
else base += "/";

String src = al.pull ("src");
if (src == null) src = "src";
src = base + src;

String cls = al.pull ("classes");
if (cls == null) cls = "classes";
cls = base + cls;

We first get the base path and fiddle it a bit, especially when not given. Then we obtain source and destination, set it to default when not there, and prepend the base (which we fiddled so we can always do so even when it does not change the path).

The repetition looks ugly. So: Factor it out. We can't purely do it with a helper function, so we need a class (this is java, everything needs to be done in a class):

class Basifier {
String base;
public Basifier (String param) {
if (param == null) base = "";
else base = param + "/";
}
public String basify (String arg, String def) {
return base + (arg != null ? arg : def)
}

and use it:

AttrList al = new AttrList (l.getParam ());
final Basifier base = new Basifier (al.pull ("dir"));
final String src = base.basify (al.pull ("src"), "src");
final String cls = base.basify (al.pull ("classes"), "classes");

Not too bad, except that we need to define a helper class and instantiate it for each invocation of ours; and we note that the useful lifetime of the object is tied exactly to the lifetime of the invocation of our funktion. In my optinion the latter is a sign that we are doing something wrong: If two things always have the same lifetime, they should really be parts of one thing.

If java had local functions and closures, we could spare the separate class:

String base = al.pull ("dir");
if (base == null) base = "";
else base += "/";

public String basify (String arg, String def) {
return base + (arg != null ? arg : def)
}

final String src = basify (al.pull ("src"), "src");
final String cls = basify (al.pull ("classes"), "classes");

We save the need for the extra object, and the need to reference it. Arguably the helper class has the advantage that the basification is encapsulated there, but it is not just that but also the default value handling for the other parameters that is done in there. Besides, the basification may also be usable in other locations.

One interesting sidepoint is that the basifier is almost, but not quite, implementable as an anonmous class:
    
final Whatever base = new Object () {
String base;
{
base = al.pull ("dir");
if (base == null) base = "";
else base += "/";
}
public String basify (String arg, String def) {
return base + (arg != null ? arg : def)
}
};
final String src = base.basify (al.pull ("src"), "src");
final String cls = base.basify (al.pull ("classes"), "classes");

The only thing that does not work is that we cannot name the type of the anonymous class for declaring base, and we need that type to be able to invoke basify on that object. In scala that actually works by type inference; the variable is just declared as a variable, and the type is assumed to be the type of the initializing expression.

Is the class Basify something that is worthy of a separate existence, or is just a little bit of code that gets factored out, and because of the needed scope of the data it works with, it is forced to become a separate class. It may also be done as a static inner class; only the option of a local function is out.

Basically in the latter case the function invocation serves as a kind of object; we could view the local function as a member function of the invocation. If that is so, we might also view the function invocation as some kind of a class, and then we might actually make it to extend another class (or implement an interface):

public void methodAsObject (String p, String q) extends Basifier {
super (p);
final String here = basify (q, "def");
..
}

It only does not work because we don't have the constructor parameter at that time.

Over the fence



By the way, other languages just do it differently. In C++ you decide whether a local variable is just a reference to an object

Basifier *base = new Basifier (param);

or whether you actually want the object itself to be a local variable:

Basifier base (param);

In the latter case the lifetime of the object is directly tied to the function invocation. This can't work in safe garbage-collected languages because you can't remove the object before you know there are no more pointers to it, and this is incompatible with with stack allocation.

Oops



So, this was planned to be a rant against excessive objectification and turns out to motivate me to actually use the basifier class. Will eventually show up here; look for "javac".

No comments: