Wednesday, October 16, 2013

Simplifying Java Method Construction for Add On Operations

Developing add on functionality with Spring Roo follows a fairly simple process; build an operation, and set up triggers for that operation. One of the predominant development tasks with building add on code for Roo is implementing the operation.  And one of the predominate parts of building those operations will be constructing methods for ITD's.

Since Java and AspectJ is in Roo's DNA, the development support for those resources is rich.  Basically the developer is responsible for defining new Java functionality via an object graph.  There are advantages to doing this.  For example all the messiness of publishing the resulting code to the file system and transaction handling have been abstracted away.  Representing Java in object graphs also allows Roo to perform integrity checks on the code.

Thus a frequent development task is adding fields, methods and annotations to Java classes.  That in turn means that we will have to construct object graphs quite frequently.  Construction of object graphs can get rather messy themselves, having to construct all the objects and then wire them together.

After crawling though Roo's source code and everything I could find on the Internet I could construct add on's following seemingly best practices.  But with following these practices the head winds are strong and the development slow.  And also just a lot of typing.  The problem with object graph construction in code is that it obscures the functionality being put together.  On one hand in code we are building objects and then assembling them together.  But also we are attempting to express a unit of functionality, so more closely we follow that structure then the easier it will be to develop and maintain our code.

For example consider some early code I wrote;


 private MethodMetadata getAssociationDeleteMethod( JavaType withType ) {

  String withClass = withType.getSimpleTypeName() ;
  String withObject = StringUtils.uncapitalize(withClass) ;

  // Specify the desired method name
  JavaSymbolName methodName = new JavaSymbolName("delete" + withClass);

  // set up method annotations
  List methodAnnotations = new ArrayList() ;

  AnnotationMetadataBuilder requestAnnotation = new AnnotationMetadataBuilder(new JavaType( "RequestMapping" ));
  requestAnnotation.addStringAttribute("value", "/{" + thisObject + "Id}/" + withObject );
  requestAnnotation.addEnumAttribute("method", new EnumDetails( new JavaType( "RequestMethod" ), new JavaSymbolName( "DELETE" )));
  requestAnnotation.addStringAttribute("produces", "text/html" ) ;
  
  methodAnnotations.add(requestAnnotation.build()) ;
  AnnotationMetadataBuilder tranactionalAnnotation = new AnnotationMetadataBuilder( new JavaType( "Transactional")) ;
  methodAnnotations.add( tranactionalAnnotation.build() ) ;

  // set up method parameters 
  List parameterTypes = new ArrayList();

  AnnotationMetadataBuilder pathVariable = new AnnotationMetadataBuilder( new JavaType( "PathVariable" )) ;
  pathVariable.addStringAttribute("value", "" + thisObject + "Id" ) ;
  List annotations = new ArrayList() ;
  annotations.add(pathVariable.build()) ;

  AnnotatedJavaType userIdParam = new AnnotatedJavaType(JavaType.LONG_OBJECT, annotations) ;
  parameterTypes.add(AnnotatedJavaType.convertFromJavaType( new JavaType( "HttpServletRequest" )));
  parameterTypes.add( userIdParam );
  parameterTypes.add(AnnotatedJavaType.convertFromJavaType( new JavaType( "Model" )));

  // Define method parameter names (none in this case)
  List parameterNames = new ArrayList();
  parameterNames.add( new JavaSymbolName( "request")) ;
  parameterNames.add( new JavaSymbolName( thisObject + "Id")) ;
  parameterNames.add(new JavaSymbolName("uiModel" )) ;
  
  InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder()
   .appendFormalLine( thisClass + " " + thisObject + " = " + thisClass + ".find" + thisClass + "(" + thisObject + "Id) ;" )
   .appendFormalLine("if (" + thisObject + " == null)" ) 
   .indent() 
   .appendFormalLine("throw new IllegalArgumentException(\"" + thisClass + " not found\");" ) 
   .indentRemove() 
   
   .appendFormalLine("String[] " + withObject + "Ids = request.getParameterValues( \"" + withObject + "Id\" ) ;")  
   .appendFormalLine("for ( int i = 0 ; i < " + withObject + "Ids.length ; i++ ) {" ) 
   .indent()
   .appendFormalLine("Long " + withObject + "Id = Long.parseLong( " + withObject + "Ids[ i ] ) ;") 
   .appendFormalLine( thisObject + ".delete" + withClass + "(" + withObject + "Id) ;" ) 
   .indentRemove() 
   
   .appendFormalLine( "}" ) 
   .appendFormalLine("return \"redirect:/" + thisClass.toLowerCase() + "s/\" + " + thisObject + " + \"/" + withObject + "s\";") ;  

  MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
    getId(), Modifier.PUBLIC, methodName, JavaType.STRING, 
      parameterTypes, parameterNames,
    bodyBuilder);
  methodBuilder.setAnnotations(methodAnnotations);
  
  return methodBuilder.build() ;
 }

This is a big method far beyond recommended practice in length.  Plus it's hard to comprehend.  We know that we are building a method and finding the body content isn't a problem.  But beyond that finding something like the method signature takes some study.

I find writing code like this rather tedious and slow.  It also takes a lot concentration to write.  On one hand one has to maintain a mental model of the object graph itself and on the other the representation of the method.  And unfortunately as a result neither the object graph or the method under construction are clear.

Even more important is consideration about how to support code like this in an enterprise environment.  As much as we would like to deny it, in an enterprise environment code is passed on from either individual to individual or even worse to a group.  Without conciseness code like this deteriorates into spaghetti rapidly.

But in the end I felt that there had to be a simpler and more concise way to assemble methods.  And since for a add on project of any size we are going to be doing a lot of method construction.

What I came up with a facade class to hide mechanics of the object graph assembly.  Instances of the facade wraps the out of the box Roo implementation.  This facade also supplies a series of methods to allow us to declare parts of the method that return the facade instance itself.  Returning the facade allows the methods to be chained together.  With the facade class the original code can now be refactored into;


 private MethodMetadata getAssociationDeleteMethod( JavaType withType ) {

  String withClass = withType.getSimpleTypeName() ;
  String withObject = StringUtils.uncapitalize(withClass) ;

  return new MethodBuilder( getId() )
   .modifier(Modifier.PUBLIC ) 
   .returns(JavaType.STRING ) 
   .named( "delete" + withClass )
    
   .parameter( new JavaType( "HttpServletRequest" ), "request" )
   .parameter(JavaType.LONG_OBJECT,   
     new MethodBuilder.Annotation("PathVariable" )
       .attribute("value", "" + thisObject + "Id" ),
     thisObject + "Id" )
   .parameter( new JavaType( "Model" ), "uiModel" )
      
   .annotation( 
     new MethodBuilder.Annotation( "RequestMapping" )
       .attribute("value", "/{" + thisObject + "Id}/" + withObject )
       .attribute("method", new EnumDetails( new JavaType( "RequestMethod" ), new JavaSymbolName( "DELETE" )))
       .attribute("produces", "text/html" ))
   .annotation( new MethodBuilder.Annotation( "Transactional") )

   .body(
    new InvocableMemberBodyBuilder()
      .appendFormalLine( thisClass + " " + thisObject + " = " + thisClass + ".find" + thisClass + "(" + thisObject + "Id) ;" )
      .appendFormalLine("if (" + thisObject + " == null)" ) 
      .indent() 
      .appendFormalLine("throw new IllegalArgumentException(\"" + thisClass + " not found\");" ) 
      .indentRemove() 
    
      .appendFormalLine("String[] " + withObject + "Ids = request.getParameterValues( \"" + withObject + "Id\" ) ;")  
      .appendFormalLine("for ( int i = 0 ; i < " + withObject + "Ids.length ; i++ ) {" ) 
      .indent()
      .appendFormalLine("Long " + withObject + "Id = Long.parseLong( " + withObject + "Ids[ i ] ) ;") 
      .appendFormalLine( thisObject + ".delete" + withClass + "(" + withObject + "Id) ;" ) 
      .indentRemove() 
    
      .appendFormalLine( "}" ) 
      .appendFormalLine("return \"redirect:/" + thisClass.toLowerCase() + "s/\" + " + thisObject + " + \"/" + withObject + "s\";"))
   .build() ;
 }
 
The above implementation is much improved, well at least in my opinion.  For example with a glance we can determine that the method has three parameters and two annotations.  Additionally with using the facade class also hides the details of coercing objects.

Here is the facade code;


package com.repik.roo.builders;

import java.util.ArrayList;
import java.util.List;

import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.model.EnumDetails;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;

/**
 * This class wraps Roo method building support into a series
 * of methods that can be changed together.  Doing this allows
 * simplier representation of method definitions.
 * 
 * @author Dan Repik
 *
 */
public class MethodBuilder {

 public static class Annotation {

  private AnnotationMetadataBuilder annotationBuilder ;
  
  public Annotation( String annotationType ) {
   annotationBuilder = new AnnotationMetadataBuilder(new JavaType( annotationType ));
  }
  
  public Annotation attribute( String name, String value ) {
   annotationBuilder.addStringAttribute( name, value ) ; 
   return this ;
  }
  
  public Annotation attribute( String name, Boolean value ) {
   annotationBuilder.addBooleanAttribute( name, value ) ; 
   return this ;
  }
  
  public Annotation attribute( String name, EnumDetails value ) {
   annotationBuilder.addEnumAttribute( name, value ) ; 
   return this ;
  }
  
  public AnnotationMetadata build() {
   return annotationBuilder.build() ;
  } 
 }
 
 private MethodMetadataBuilder methodBuilder;

 private List annotationList = new ArrayList() ;

 private List parameterTypes = new ArrayList();
 
 private List parameterNames = new ArrayList();

 public MethodBuilder( String metadataId ) {
  methodBuilder = new MethodMetadataBuilder( metadataId ) ;
 }
 
 public MethodBuilder modifier( int modifier ) {
  methodBuilder.setModifier(modifier) ;
  return this ;
 }
 
 

 public MethodBuilder parameter( AnnotatedJavaType parameterType, String parameterName ) {
  parameterTypes.add( parameterType ) ;
  parameterNames.add( new JavaSymbolName(parameterName )) ;
  return this ;
 }
 
 public MethodBuilder parameter( JavaType parameterType, String parameterName ) {
  return parameter( AnnotatedJavaType.convertFromJavaType( parameterType ), parameterName ) ;
 }

 public MethodBuilder parameter( JavaType parameterType, Annotation parameterAnnotation, String parameterName )  {
  return parameter(new AnnotatedJavaType(parameterType, parameterAnnotation.build()), parameterName ) ;

 }
 
 public MethodBuilder annotation( Annotation annotation ) {
  annotationList.add( annotation.build() ) ;
  return this ;
 }
 
 public MethodBuilder body( InvocableMemberBodyBuilder body ) {
  methodBuilder.setBodyBuilder( body ) ;
  return this ;
 }
 

 
 public MethodBuilder returns( JavaType returnType ) {
  methodBuilder.setReturnType(returnType) ;
  return this ;
 }
 
 public MethodBuilder named( String name ) {
  methodBuilder.setMethodName( new JavaSymbolName( name )) ;
  return this ;
 }

 public MethodMetadata build() {
  methodBuilder.setAnnotations( annotationList ) ;
  methodBuilder.setParameterTypes(parameterTypes) ;
  methodBuilder.setParameterNames(parameterNames) ;
  return methodBuilder.build() ;
 }
}


As you can see there really isn't a lot of code here, but a little can go along way towards improving the readability of the code.

No comments:

Post a Comment