Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
300 views
in Technique[技术] by (71.8m points)

design patterns - Difference between @Delegate, @Mixin and Traits in Groovy?

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

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 @Delegates 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.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...