Would someone explain when I would want to use Groovy Traits vs. Mixins (@Mixin) vs. Delegates (@Delegate)? Maybe some trade-offs and design concerns would help.
They all seem to allow for reusing multiple "classes" of behavior. Thanks. :-)
This SO thread was helpful too: Difference between @Delegate and @Mixin AST transformations in Groovy
I agree, they all seem to allow reusing multiple "classes" of behaviour. There are differences, though, and understanding these will probably aid your decision.
Before providing a brief summary/highlight of each feature and examples of suitable usage, let's just summarize on the conclusion of each.
Conclusion / typical usage:
And now, let's look into each of these with a little bit more detail.
@Delegate
Inheritance is over-used in many cases. That is, it is often improperly used. Classic examples in Java are
extending input streams, readers or the collection classes.. For most of these, using inheritance is too
tightly coupled with the implementation. That is, the actual implementation is written so that one of the
public methods actually use another. If you override both, and you call super
, then you might get unwanted
side-effects. If the implementation changes in a later version, then you will have to update your handling of
it as well.
Instead, you should strive to use composition over inheritance.
Example, a counting list that counts the elements added to a list:
class CountingList<E> {
int counter = 0
@Delegate LinkedList<E> list = new LinkedList<>()
boolean addAll(Collection<? extends E> c) {
counter += c.size()
list.addAll(c)
}
boolean addAll(int index, Collection<? extends E> c) {
counter += c.size()
list.addAll(index, c)
}
// more add methods with counter updates
}
In this example, the @Delegate
removes all the tedious boiler-plate code for all public methods that you
want to leave "as-is", i.e. methods are added that simply forwards the call to the underlying list. In addition,
the CountingList
is separated from the implementation so that you don't have to care whether one of these
methods is implemented by calling the other. In the example above, that is actually the case, since
LinkedList.add(Collection)
calls LinkedList.add(int, Collection)
, so it would not be as straight-forward
to implement using inheritance.
Summary:
@Delegate
s to one class.
CountingList
in the example above) are not instances of the delegate class.
CountingList
is not an instance of LinkedList
.@Mixin
The @Mixin
transform will be deprecated with groovy 2.3, due to the upcoming traits support. This provides a
hint that everything that is possible to do with @Mixin
, should be possible to do with traits instead.
In my experience, @Mixin
is sort of a mixed blessing. :)
It is, by the core developers admission, bug-ridden with "hard-to-solve" bugs. That's not to say that it's been "useless", far from it. But if you have the opportunity to use (or wait for) groovy 2.3, then you should use traits instead.
What the AST transform does, is simply to add the methods from one class into another. For instance:
class First {
String hello(String name) { "Hello $name!" }
}
@Mixin(First)
class Second {
// more methods
}
assert new Second().hello('Vahid') == 'Hello Vahid!'
Summary:
Second
is not an instance of First
Runtime mixin
Runtime mixins and the @Mixin
transform are quite different, they solve different use-cases and are used
in totally different situations. Since they have the same name, it's easy to confuse one with the other, or to
think that they are one and the same. Runtime mixins, however, are not deprecated in groovy 2.3.
I tend to think about runtime mixins as the way to add methods to existing classes, such as any class in the JDK. It's the mechanism used by Groovy to add extra methods to the JDK.
Example:
class MyStringExtension {
public static String hello(String self) {
return "Hello $self!"
}
}
String.mixin(MyStringExtension)
assert "Vahid".hello() == 'Hello Vahid!'
Groovy also have a nice extension module feature, where you don't need to manually perform the mixin, instead groovy does it for you as long as it finds the module descriptor in the correct location in the classpath.
Summary:
Traits
Traits are new to groovy 2.3.
I tend to view these traits as something between the familiar interface and class. Something akin to a "light-weight" class. They are dubbed "interfaces with default implementations and state" in the documentation.
Traits are similar to the @Mixin
transform that they replace, but they are also more powerful. For starters, they
are much more well-defined. A trait cannot be instantiated directly, just like an interface, they need an implementing
class. And a class may implement many traits.
A simple example:
trait Name {
abstract String name()
String myNameIs() { "My name is ${name()}!" }
}
trait Age {
int age() { 42 }
}
class Person implements Name, Age {
String name() { 'Vahid' }
}
def p = new Person()
assert p.myNameIs() == 'My name is Vahid!'
assert p.age() == 42
assert p instanceof Name
assert p instanceof Age
The immediate difference between traits and @Mixin is that trait
is a language keyword, not an AST transform.
Further, it can contain abstract methods that needs to be implemented by the class. Further, a class can implement
several traits. The class implementing a trait is an instance of that trait.
Summary: