Groovy Scripts - Reusable Code

Let's start with the following Groovy script which reads and prints all the properties from a file.

import java.nio.file.Path

Properties loadProperties() {
    Properties properties = new Properties()
    Path propertyFilePath = Path.of('my.properties')
    propertyFilePath.withInputStream { properties.load(it) }
    properties
}

Properties properties = loadProperties()
properties.each { println it }

The script needs 'my.properties' file with the following content.

key1=value1
key2=value2

Suppose we also want a script that accepts the property key as a command-line argument and prints out the property value along with the key. We could write that script as follows.

import java.nio.file.Path

Properties loadProperties() {
    Properties properties = new Properties()
    Path propertyFilePath = Path.of('my.properties')
    propertyFilePath.withInputStream { properties.load(it) }
    properties
}

Properties properties = loadProperties()
println properties.find { it.key == args[0] }

You might have already noticed the duplicate code. However, what is prominent here is that the logic to load the properties from the file is represented in multiple places leading to the violation of DRY (Don't Repeat Yourself) principle.

Let's work towards adhering to the DRY principle. It's obvious the logic to load the properties cannot be in one of the two scripts.

One option is to extract the common piece of code into a class as follows.

import java.nio.file.Path

class PropertyUtil {
    static Properties loadProperties() {
        Properties properties = new Properties()
        Path propertyFilePath = Path.of('my.properties')
        propertyFilePath.withInputStream { properties.load(it) }
        properties
    }
}

I have moved the loadProperties method into a new class PropertyUtil in the same default package. The scripts would look as follows with our refactoring.

Properties properties = PropertyUtil.loadProperties()
properties.each { println it }
Properties properties = PropertyUtil.loadProperties()
println properties.find { it.key == args[0] }

The above code works as expected. However, since the method loadProperties is not in a script, it would not have any of the context information of the script.

Let's make a case for using script capabilities in the reusable method loadProperties. Suppose we don't want to hard-code the name of the property file. Instead, we want to pass the name of the property file as a command-line argument. The code would look as follows.

import java.nio.file.Path

class PropertyUtil {
    static Properties loadProperties(String propertyFile) {
        Properties properties = new Properties()
        Path propertyFilePath = Path.of(propertyFile)
        propertyFilePath.withInputStream { properties.load(it) }
        properties
    }
}

And the script would look as follows.

Properties properties = PropertyUtil.loadProperties(args[0])
properties.each { println it }

For brevity, I am not showing the second script here onwards.

Here, we had to pass the command-line argument holding the name of the property file as an argument to loadProperties. Instead, you could take the following approach.

Lets make the class PropertyUtil extend groovy.lang.script as follows

import java.nio.file.Path

abstract class PropertyUtil extends Script {
    Properties loadProperties() {
        Properties properties = new Properties()
        Path propertyFilePath = Path.of(args[0])
        propertyFilePath.withInputStream { properties.load(it) }
        properties
    }
}

Note how we are not passing the property file name anymore to loadProperties, and relying upon the script capabilities to get access to command-line arguments.

Our script will be updated as follows.

import groovy.transform.BaseScript

@BaseScript PropertyUtil propertyUtil
Properties properties = loadProperties()
properties.each { println it }

The BaseSCript AST transformation makes sure that the generated class for our script extends from PropertyUtil rather than Script. Hence the method loadProperties is available to our script.

The above code violates the Liskov Substitution Principle (LSP) which does not make me happy. The composition strategy would be a better design approach here than inheritance.

Let's modify the code to fix the LSP violation.

import java.nio.file.Path

abstract class PropertyUtil extends Script {
    protected Properties props

    void loadProperties() {
        Properties properties = new Properties()
        Path propertyFilePath = Path.of(args[0])
        propertyFilePath.withInputStream { properties.load(it) }
        props = properties
    }

    abstract void useProperties()

    @Override
    Object run() {
        loadProperties()
        useProperties()
    }
}

In our script, we need to supply the implementation for the abstract method useProperties as follows.

import groovy.transform.BaseScript
@BaseScript PropertyUtil propertyUtil
props.each { println it }

Here, whatever code is in the body of the script becomes the implementation for the abstract method defined in the base script. If you are familiar with design patterns, you would have already recognised the template method pattern.

One last thing about the Groovy scripts - if you have a very complex set of command-line arguments I would suggest you opt for libraries like picocli than a conventional Groovy script.

Show Comments