Monday, February 8, 2010

Elegantification is possible even in Java

Sometimes it's the small things that make you smile. (That's so. Sometimes at least ;-)

The unfavorable situation

In the Usus UI, we have several tables displaying code proportions information, and in order to simplify the code that configures such a table, we have a common TreeViewer that reads column information from an enum, where each of the enum's elements represents a table column and its meta data, such as the header string, the column weight and the text alignment in the column's cells. Thus, the table description for the Usus Cockpit view looked like this:
enum CockpitColumnDesc implements IColumnDesc {

INDICATOR( "Indicator", LEFT, 56, true ) {
public String getLabel( CodeProportion element ) {
return element.getMetric().getLabel();
}
},
SQI( "SQI", 10, false ) {
public String getLabel( CodeProportion element ) {
// ...
}
},
// ... more enum fields

private final String headLabel;
private final int weight;
private final boolean hasImage;

CockpitColumnDesc( String headLabel, ColumnAlignment align, int weight, boolean hasImage ) {
this.headLabel = headLabel;
this.align = align;
this.weight = weight;
this.hasImage = hasImage;
}

CockpitColumnDesc( String headLabel, int weight, boolean hasImage ) {
this( headLabel, RIGHT, weight, hasImage );
}

public int getWeight() {
return weight;
}

public String getHeadLabel() {
return headLabel;
}
// ...
}
Now, having about a dozen or so column description enums like this, it became a little unwieldy to add more column information (such as the alignment, i.e. LEFT, RIGHT or CENTER). Each of the enums needed another field that kept the alignment, a getter and another constructor if a sensible default value was to apply. In other words, in order to add alignment information, we still had to write a bunch of code lines into each of the enums (that is, a bunch of code lines that was practically identical for each of them). That looked unelegant to me. The elegant solution was to use a custom annotation type.

Making it look nicer

Annotations are normally used to attach meta data to language elements (classes, methods, fields), so that development tools can read them and do something sensible with the information. For instance, the well-known @SuppressWarnings annotation, when put above a method declaration, tells the Java compiler to shup up about some thing it might have complained about otherwise. (And gentle reader, you won't be surprised that this feature is misused every so often...)

On the other hand, you can make annotation information available at runtime, and read it via reflection mechanisms. Let's say you declare a new annotation type:
@Target( value = { ElementType.FIELD } )
@Retention( RetentionPolicy.RUNTIME )
public @interface UsusTreeColumn {
String header() default "";

// column weight (percentage of overall width in the table that this column takes)
int weight() default 5;

ColumnAlignment align() default ColumnAlignment.LEFT;

}
The bit about the RetentionPolicy tells the compiler to include the annotation information in the compiled code, so that it can be loaded at runtime. If someone uses your annotation like so:
@UsusTreeColumn( header = "SQI", align = RIGHT, weight = 10 )
SQI( false ) {
public String getLabel( CodeProportion element ) {
// ...
}
}
you can reach the info in the annotation this way:

        try {
Field field = loadField( "SQI" );
for( Annotation annotation : field.getAnnotations() ) {
if( annotation instanceof UsusTreeColumn ) {
UsusTreeColumn column = (UsusTreeColumn)annotation;
String header = column.header();
// ...
}
}
} catch( NoSuchFieldException nosufex ) {
// ...
}

// ...

private Field loadField() throws NoSuchFieldException {
Class enumClass = columnDescEnumValue.getClass();
if( enumClass.isAnonymousClass() ) {
enumClass = enumClass.getEnclosingClass();
}
return enumClass.getDeclaredField( columnDescEnumValue.toString() );
}

Basically, you have to find the language element (in this case a field from the enum type) that has the annotation attached to it, and then getAnnotations() from it. The way you find it is via reflection as usual. Once you have the annotation, you can cast it to the interface type you declared (in this case UsusTreeColumn) and simply use it like any other Java object. In essence, that is what our tree viewer now does. The enums, on the other hand, look compact and much more readable:

enum CockpitColumnDesc implements IColumnDesc {

@UsusTreeColumn( header = "Indicator", weight = 56 )
INDICATOR( true ) {
public String getLabel( CodeProportion element ) {
return element.getMetric().getLabel();
}
},
@UsusTreeColumn( header = "SQI", align = RIGHT, weight = 10 )
SQI( false ) {
public String getLabel( CodeProportion element ) {
// ...
}
}

// ...

private final boolean hasImage;

CockpitColumnDesc( boolean hasImage ) {
this.hasImage = hasImage;
}

public boolean hasImage() {
return hasImage;
}
}

Most of the information is compressed in the annotations, though it is still well compiler-checked; and sensible defaults can be used, so that we can leave out parameters in the annotations if we like. (The code for all the enums that describe columns in the Usus UI is now less than half that it was before. Not that code size reduction is everything that matters — but since the new code brings the same information in a much more compact notation, it's really more readable now.)

1 comment:

  1. I don't get it. You replaced the enum code by reflections in an annotation? All that matters is code size reduction?
    Is it really shorter altogether? And more elegant? - I am not sure...

    ReplyDelete