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.
respond | link |
/code
|