On Aug 13, 2009, at 11:50 PM, Adam Murdoch wrote:
Hi,
We currently apply convention mappings to various Task subclasses,
and for RepositoryHandler. We also indirectly apply them to the root
CopySpec of the Copy task. I want to be able to do this for (new
domain object) SourceSet. For example, SourceSet might have a
classesDir property, which would default to $buildDir/
$classesDirName. I could just hardcode this in, but it would be nice
to generalise the solution and make it cross-cutting.
We also have a similar problem with dynamic properties and methods.
Currently Project and Task can be extended at runtime using
convention objects and dynamic properties. We would benefit from
making Configuration and SourceSet extensible as well. For example,
the maven plugin can do a much better job if it can extend
Configuration to have a bunch of maven-specific properties and
methods.
Which is also true for Dependency and Artifact objects.
Similarly, the groovy, scala, or code-quality plugins could add
groovy, scala, checkstyle, etc specific properties to a SourceSet.
I would like to come up with an approach which allows us to apply
the convention mapping and extensible object capabilities to pretty
much any domain object.
I think that we should aim to make these capabilities usable from
both Java code and Groovy code. This is because Gradle itself will
be a heavy (this only?) user of these mechanisms, and we want to be
free to use Java code as appropriate. Our current hand-coded
implementations allow this.
I can see a few possible approaches:
1. The domain object class, or an abstract superclass, implements
the appropriate interface, such as IConventionAware. This is the
hand-coded approach we use now.
public interface SourceSet extends IConventionAware { }
public class DefaultSourceSet implements SourceSet {
... implements everything
}
SourceSet set = new DefaultSourceSet()
set.conventionMapping('classesDir', {some code})
2. The domain object class is declared abstract and is declared as
implementing the appropriate interface. We generate an implementing
subclass which implements the missing bits to mix in the desired
behaviour. The generation might happen at compile time or load time
or runtime. It might be done in any number of ways. The how doesn't
matter too much at this stage.
public interface SourceSet extends IConventionAware { }
public abstract class DefaultSourceSet implements SourceSet {
... implements the SourceSet specific methods, but not the
IConventionAware methods
}
SourceSet set = generator.newInstance(DefaultSourceSet.class)
set.conventionMapping('classesDir', {some code})
3. The domain object is a pojo, and does not implement any special
interfaces. We generate a subclass which implements the appropriate
interface and mixes in the behaviour.
public interface SourceSet { }
public class DefaultSourceSet implements SourceSet {
... implements SourceSet specific methods
}
SourceSet set = generator.newInstance(DefaultSourceSet.class)
IConventionAware convAware = (IConventionAware)set
convAware.conventionMapping('classesDir', {some code})
4. The domain object is a pojo, and we add groovy meta-methods and
properties to mix in the behaviour. Java classes would use some
helper class to access the appropriate behaviour.
public interface SourceSet { }
public class DefaultSourceSet implements SourceSet {
... implements SourceSet specific methods
}
SourceSet set = generator.newInstance(DefaultSourceSet.class)
// from groovy, a dynamic method call
set.conventionMapping('classesDir', {some code})
// from java
IConventionAware convAware = generator.asConventionAware(set)
convAware.conventionMapping('classesDir', {some code})
Option 1 advantages
- is simple
- the capabilities of a class are declared and documented as part of
the API
Disadvantages:
- we have to stamp out the same boiler plate for each capability
applied to each type of domain object.
- the capabilities applied to a given class is static
- from painful experience, this approach runs into various different
Groovy bugs.
Option 2 advantages:
- allows the capabilities to be declared and documented as part of
the API
- don't have to implement the same boiler plate everywhere.
Disadvantages:
- the capabilities are static.
- quite intrusive on the domain class.
Option 3 advantages:
- allows capabilities to be applied to pretty much any domain class,
including 3rd party classes, with very minimal impact on the domain
class.
- the behaviour is mixed-in at the bottom most subclass, so it is
much less likely to run into Groovy bugs (this is just my gut feel).
- we can use Groovy for now to implement the subclass, but we are
not tied to a Groovy-based implementation, so we could later use
bytecode generation or aspectj or similar.
Disadvantages:
- we lose the static declaration and documentation of the
capability. We would need better documentation than we could
otherwise get away with for this.
- moves some of the complexity from implementer of a domain class to
the client of a domain class
Option 4 is really just a variation on option 3, but ties the
capabilities explicitly to Groovy.
My preference is for option 3.
I also prefer 3 or 4. I think not being intrusive will be more and
more important as people enhance Gradle. Regarding the documentation
aspect. It will not remain the only area were we will inject
additional dynamic methods. So we have to find anyway a reasonable to
document this. And as this is a general feature I think this is rather
easy to communicate.
I guess 3.) will be harder to implement then 4.). As a general
strategy we might decide not to bother about using Groovy under the
hood unless there is a concrete endeavor to implement an additional
build script engine in another language.
- Hans
--
Hans Dockter
Gradle Project Manager
http://www.gradle.org
---------------------------------------------------------------------
To unsubscribe from this list, please visit:
http://xircles.codehaus.org/manage_email