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:
- @Delegate: Used to add all the functionality of the delegate class, but still avoid tightly coupling to the
actual implementation. Let's you achieve composition over inheritance.
- @Mixin: Deprecated with groovy 2.3. Simple way to add methods from one or more classes into your class. Bug-ridden.
- Runtime mixin: Add one or more methods into any existing class, e.g. a class in the JDK or a 3rd party library.
- Traits: New in groovy 2.3. Well-defined way to add one or more traits to your class. Replaces @Mixin. The only
one of these where added methods are visible in Java classes.
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:
- Provides default implementations for all public methods in the delegated object.
- Methods with same signature that are explicitly added, take precedence.
- Implicitly added methods are not visible in Java.
- You can add several
@Delegate
s to one class.
- but if you do, you should consider whether that is really desirable.
- what about the diamond problem, i.e. if you have multiple methods in the delegates with the same signature?
- The class with delegates (
CountingList
in the example above) are not instances of the delegate class.
- I.e.
CountingList
is not an instance of LinkedList
.
- Use to avoid tightly coupling through inheritance.
@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:
- Adds methods from one class into another.
- Use in groovy <2.3 for simple addition of methods from one class to another
- don't add to "super" classes (at least, I've had problems with that)
- Bug-ridden
- Deprecated from groovy 2.3
- Implicitly added methods are not visible in Java.
- The class that gets another class mixed in, are not instances of that other class
- I.e.
Second
is not an instance of First
- You can mix in several classes into one other class
- what about the diamond problem, i.e. if you have methods in the mixed in classes with the same signature?
- Use as a simple method of adding the functionality of one class into another in groovy <2.3
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:
- Add methods to any existing class
- any classes in the JDK
- any 3rd party classes
- or any of your own classes
- Overrides any existing method with the same signature
- Added methods are not visible in Java
- Typically used to extend existing/3rd party classes with new functionality
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:
- Traits provide an interface with implementation and state.
- A class can implement multiple traits.
- Methods implemented by a trait are visible in Java.
- Compatible with type checking and static compilation.
- Traits can implement interfaces.
- Traits can not be instantiated by themselves.
- A trait can extend another trait.
- Handling of the diamond problem is well-defined.
- Typical usage:
- add similar traits to different classes.
- (as an alternative to AOP)
- compose a new class from several traits.