Brian Slesinsky's Weblog
 
   

Wednesday, 01 Oct 2003

Half-Bean Questions and Answers

Thanks to Cedric Beust for being the first one to respond to my article about Half-Beans. Apparently I've glossed over some important points, so let me expand on them a bit.

What does this have to do with functional programming?

In a pure functional language, all values are immutable and calculations are done through function application. In other words, a program in a functional language doesn't perform an action, but instead constructs an answer.

The part of functional programming that I try to emulate when programming in Java is the widespread use of immutable values, because I think it makes programs easier to reason about and more thread-safe. I'm not going to be strict about this by any means - Java isn't designed for it. However, I do think immutable objects could be used much more widely than they are in the code I've seen. Making Strings immutable was a wonderful choice (says this former C programmer) that I think can be emulated in all sorts of Java programming.

When writing a program in Java, my goal is not to eliminate the use of mutable values, but rather to encapsulate their use. For example, here's a simple method to concatenate a list of items into a string:

    public static String join(List items, String separator) {
        StringBuffer result = new StringBuffer();
        for (Iterator it = items.iterator(); it.hasNext();) {
            result.append(it.next().toString());
            if (it.hasNext()) result.append(separator);
        }
        return result.toString();
    }

Although the implementation of this method uses a StringBuffer, the interface is purely functional. The StringBuffer is created, used and discarded without any outside method knowing. As far as the caller is concerned, join() is just a special kind of String constructor.

Doesn't this cause maintenance problems? When I add a new property to Album, I need to remember to update AlbumBuilder too.

Yes, there is a certain amount of maintenance, but it would need to be done with a regular JavaBean, and I find that the Half-Bean pattern makes it harder to forget to do the maintenance and easier to discover the bug if it's not done.

For example, suppose I add a producer field to Album. If I add a final instance variable to Album and forget to set it in the constructor, the code won't compile. To fix the Album constructor, I have to put a corresponding field in AlbumBuilder - and that reminds me to update the constructors there, add setProducer() and update isComplete(). Once isComplete() is updated, any code that uses an AlbumBuilder needs to call setProducer() or there will be a runtime error in newAlbum(). Searching for usages of newAlbum() will find all the appropriate locations. Otherwise, this is the sort of bug that shows up in unit tests - you can't execute AlbumBuilder code without it showing up.

This isn't quite as good as a compile-time error, but it's better than an ordinary JavaBean. Because there's no checkpoint where you declare the JavaBean to be complete, if you forget to call a setter, nobody notices until down the road when an uninitialized property gets used, at which point the constuctor code where the bug actually lies probably isn't in the stack trace and might be difficult to track down.

Why can't you construct an Album without using AlbumBuilder, just like you can construct Strings without using StringBuffer?

From the caller's perspective, there's no reason why not. Album could have multiple constructors for creating albums from various sources, which use an AlbumBuilder behind the scenes. For example, here's a constructor to read an album from a property map (some error-checking omitted):

    public Album(Map props, String prefix) {
        this( readProps(props, prefix) );
    }
 
    private static AlbumBuilder readProps(Map props, String prefix) {
        AlbumBuilder b = new AlbumBuilder();
        b.setAlbumID( getIntProp(props, prefix, "id"));
        b.setAlbumName( getProp(props, prefix, "name"));
        b.setArtist( new Artist(getProp(props, prefix, "artist")) );
 
        for (int trackNum=1; ; trackNum++) {
            String trackProp = getProp(props, prefix, "track"+trackNum);
            if(trackProp==null) break;
            b.addTrack( trackProp );
        }
 
        return b;
    }
 
    private static int getIntProp(Map props, String prefix, String propName) {
        return Integer.parseInt( getProp(props, prefix, propName) );
    }
 
    private static String getProp(Map props, String prefix, String propName) {
        return (String)props.get(prefix+"."+propName);
    }

In fact, when writing in a functional style, it becomes very natural to do lots of work in constructors; most code is concerned with constructing objects in one way or another.

Couldn't I just chain methods together like when working with strings?

Strings are somewhat of a special case because they have hardly any constraints. When you write " hello ".trim().toUpperCase(), all of the intermediate results are valid strings as far as the String class is concerned (even though they might not be valid in your particular application).

But with more complicated types of constraints (such as on multiple properties at once), it becomes very awkward to preserve all of the constraints in the middle of a modification. For a simple example, something like album.clearTracks().addTrack("ABC") wouldn't work unless I removed the constraint that all Albums must have at least one track. Using an AlbumBuilder allows me to tighten up constraints on Albums considerably without making constructing them too awkward.