June 15, 2018

Groovy Goodness: Implement Interface And Abstract Methods Automatically

A lot of new AST transformation annotations are added in Groovy 2.5.0. One of them is the @AutoImplement annotation. If we apply this annotation to our class dummy implementations for abstract methods in superclasses or methods in implemented interfaces are created. This can be useful to have something in place and then gradually write real implementations for the abstract or interface methods. The transformation will not alter any method that is already implemented by custom code.

When we apply the @AutoImplement annotation the default implementation for an abstract method from a superclass or method from a interface is simple. If the method has a return type the default value of that return type is returned. For example false for a boolean and null for an object type. But the @AutoImplement annotation has some attributes we can use to change the default implementation. We can set the exception attribute and assign a exception type. The implementation of the methods is than to throw that exception when the method is invoked. With the optional message attribute we can set the exception message. Finally we can use the code attribute to define a Closure with statements that will be called as the implementation of abstract and interface methods.

In the following example we have an interface Creator and create several classes that implement this interface and apply the @AutoImplement annotation with different attribute values:

import groovy.transform.AutoImplement
import static groovy.test.GroovyAssert.shouldFail

// Sample class with two simple properties.
class Course {
    String name, location

// Interface with a single method to 
// create an object based on Map argument.
interface Creator<R> {
    R create(Map args)

// Use AutoImplement annotation to create object
// implementing the Creator interface. The compiled
// class will have an implementation for the create
// method. The return value is the default of the
// return type, in our case null.
class DefaultCourseCreator implements Creator<Course> { }

def defaultCreator = new DefaultCourseCreator()
assert defaultCreator.create(name: 'Groovy', location: 'Tilburg') == null

// When we use the AutoImplement annotation, only
// methods that are not implemented by ourselves
// will have an auto implementation.
class ImplCourseCreator implements Creator<Course> { 
    Course create(Map args) {
        new Course(args)

def creator = new ImplCourseCreator()
assert creator.create(name: 'Groovy', location: 'Tilburg') == new Course(name: 'Groovy', location: 'Tilburg')

// Instead of returning the default value for the return type,
// we can throw an exception as default implementation.
// We can specify the type of exception and optionally the
// exception message.
@AutoImplement(exception = UnsupportedOperationException, 
               message = 'Not supported by NotSupportedCourseCreator')
class NotSupportedCourseCreator implements Creator<Course> { }

def creatorUnsupported = new NotSupportedCourseCreator()

def exception = shouldFail(UnsupportedOperationException) {
    creatorUnsupported.create(name: 'Groovy 101')
assert exception.message == 'Not supported by NotSupportedCourseCreator'

// We can use the code attribute of the AutoImplement annotation to
// specify a Closure. The Closure is used as implementation for the
// method from the interface.
// In this case we log a warning and return null as value. Notice
// we can access the log variable added by the Log annotation in 
// the Closure of the code attribute.
@AutoImplement(code = { log.warning('Method needs implementation'); return null })
class ImplementCodeCourseCreator implements Creator<Course> { }

def creatorWarning = new ImplementCodeCourseCreator()
assert !creatorWarning.create(name: 'Groovy') // Prints log message "WARNING: Method needs implementation"

Written with Groovy 2.5.0.