Skip to end of metadata
Go to start of metadata

Gradle wants to offer out-of-the box experience but also maximum freedom. It wants offer a framework, but also a toolset. Depending on the different use cases.

How to design for these requirement?

We start from bad to good.

The tasks will do it

All configuration information is in the tasks. The API of the tasks objects is used by other tasks to get common information.

This is problematic for a number of reasons.

  • There is configuration information which does not naturally belong to any particular task. This information might be needed by a couple of tasks, but it has to be put in some task. This is unintuitive.
  • The task API is part of the domain model. This means the build depends on the type of the task. If the user wants to replace the task('compile'), which is per default of type Compile, with a custom task of some other type, this breaks the out-of-the box functionality. This is a fragile smell.
  • The advantage of this approach is, that the configuration information is stored only once in some task. If you change it there, this change is taken up by all the other task which use the changed task as information source.

Layout objects to the rescue

We can introduce layout objects. They contain all the shared information needed by the tasks. We could now pass this layout object by reference to all our task. The tasks don't have own properties for the information contained in the tasks. What is the problem here? Let's look at our Resources class (which is a task). It would not have a property targetDir, it just uses the layout object to get this information. This is fine for the Gradle framework behavior. But what if someone want to use Gradle instead/additionally as a toolset? You want to create you custom task of type Resources. The only property you want to set is targetdir, but you have to create your own layout object, for configuring just the targetdir. This is cumbersome.

Another bad approach with layout objects

We don't pass the layout object to the tasks. The tasks have there own properties. What we do in the plugin, when we create the tasks, is to set the task properties with the help of the layout object. This is problematic. The first question is when does the plugin sets the properties of the tasks. Before of after the users gradle.groovy script is evaluated? If we set the task properties before, any change the user does to the layout object are not reflected in the tasks. If we set it after the evaluation, the properties of the tasks are hidden to the users gradle.groovy script. Furthermore changes the user is doing the tasks are possibly overwritten.
Obviously both approaches are very bad.

Making the framework and the toolset happy

We pass the layout object by reference to the tasks. Yet the tasks have there own properties. For example the Resources task has the property targetDir and a reference to the layout object. We now overwrite the getter for the targetDir property. The getter returns the value of the targetDir property, if the property is not null. Otherwise we look into the layout object for the value. The way we have implemented it, the tasks don't depend on a specific type of layout object. Each task has a map, which maps a propertyName of the task to a closure, that has a layout object as an argument. It is the job of the plugin developer to:

  • pass the layout object to the task
  • declare the mapping, how the layout objects properties are mapped to the task properties. For example: resources.layoutMapping(targetDir: { it.testClassesDir }), where it is the layout object.
    We have achieved that the tasks can be used stand alone by setting directly the task properties. Within a framework(plugin), which coordinates the actions of multiple plugins, we can use a shared layout object to configure the tasks. By not relying on the type of the layout object, we don't bind our tasks to a specific plugin. Users can develop custom plugins with there custom layout objects and still can use the tasks Gradle provides.
Labels: