Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rethinking API design with traits

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Cédric Champeau Cédric Champeau
September 09, 2014

Rethinking API design with traits

Talk about #groovylang traits given during SpringOne2GX 2014 in Dallas. #s2gx #springio

Avatar for Cédric Champeau

Cédric Champeau

September 09, 2014
Tweet

More Decks by Cédric Champeau

Other Decks in Technology

Transcript

  1. © 2014 SpringOne 2GX. All rights reserved. Do not distribute

    without permission. Rethinking API design with traits By Cédric Champeau
  2. #s2gx #groovylang @CedricChampeau About me Pivotal employee Core Groovy committer

    Compilation configuration Static type checking & Static compilation Traits, new template engine, ... Groovy in Action 2 co-author Misc OSS contribs (Gradle plugins, deck2pdf, jlangdetect, ...) 2
  3. #s2gx #groovylang @CedricChampeau Groovy is Open-Source • Licensed under APL2

    • 100+ contributors • 10000+ commits • 1.7+ million downloads in 2013 • On GitHub since summer 2011 • Dependency of 25000+ OSS projects 4
  4. #s2gx #groovylang @CedricChampeau Life without traits, pre Java 8 •

    Contracts are defined through interfaces • Interfaces are mandatory in a static type system • Base behavior defined through abstract classes • To implement multiple contracts, you need multiple abstract classes • … or you can use duck typing in Groovy 6
  5. #s2gx #groovylang @CedricChampeau Life without traits, pre Java 8 •

    Even in Groovy... • Problems with inheritance • Hard to determine whether a class explicitly or implicitly implements a contract 7
  6. #s2gx #groovylang @CedricChampeau @Mixin • Deprecated in Groovy 2.3 •

    Combines static and dynamic features to achieve runtime mixins 8 class Cat { void meow() { println 'Meow!' } } @Mixin(Cat) class YoutubeDog { void bark() { println 'Woof!' } } def dog = new YoutubeDog() dog.meow() dog.bark()
  7. #s2gx #groovylang @CedricChampeau @Mixin • Mixed in class is not

    an instance of the mixin • Problems with inheritance • Memory • Difference between the mixed instance and the mixin instance • Buggy (really) 9
  8. #s2gx #groovylang @CedricChampeau Delegation • Delegation vs inheritance • Delegation

    keeps concerns separate • Cleaner responsibility model • Not always suitable • Foo “is a Bar” → inheritance • Foo “acts as a Bar” → composition • Optionally requires interfaces 10
  9. #s2gx #groovylang @CedricChampeau @Delegate : composition model 11 /* An

    API-limited version of a Stack */ class StackOnly<T> { Stack<T> delegate = new Stack<>() void push(T e) { delegate.push(e) } T pop() { delegate.pop() } } def t = new StackOnly<String>() t.push 'a' t.push 'b' assert t.pop() == 'b'
  10. #s2gx #groovylang @CedricChampeau @Delegate : composition model 12 /* An

    API-limited version of a Stack */ class StackOnly<T> { @Delegate(interfaces=false, includes=['push','pop']) Stack<T> delegate = new Stack<>() } def t = new StackOnly<String>() t.push 'a' t.push 'b' assert t.pop() == 'b' assert t.get(0) == 'a' // will fail because get is not delegated
  11. #s2gx #groovylang @CedricChampeau @Delegate : composition model • Excellent fit

    when the delegating object requires a subset of the features of the delegate • Delegation + interfaces used as the poor man's multiple inheritance solution 13
  12. #s2gx #groovylang @CedricChampeau Groovy specific : Extension methods • URL#getText(String

    encoding) • Allows adding behavior to existing classes • Supported by @CompileStatic • Add your own thanks to extension modules – See http://docs.groovy-lang.org/latest/html/documentation/#_extension_modules • Not visible from Java 14
  13. #s2gx #groovylang @CedricChampeau Java 8 default methods • Some kind

    of traits • An interface • With default implementation 15 public interface Greeter { default void greet() { System.out.println("Hello!"); } } // Usable from Groovy class DefaultGreeter implements Greeter { }
  14. #s2gx #groovylang @CedricChampeau Java 8 default methods • Stateless •

    No state = No diamond issue • Virtual 16 class VirtualGreeter implements Greeter { @Override void greet() { println "Virtual Hello!" } } • Backward compatible • Groovy does not support writing default methods
  15. #s2gx #groovylang @CedricChampeau Traits • Similar to Java 8 default

    methods • Supported in JDK 6, 7 and 8 • Stateful • Composable • Not really virtual... 18
  16. #s2gx #groovylang @CedricChampeau Traits : the basics • Declared through

    the “trait” new keyword • Requires Groovy 2.3+ 19 trait Flying { void fly() { println "I'm flying!" } } trait Quacking { void quack() { println 'Quack!' } } class Duck implements Flying, Quacking {}
  17. #s2gx #groovylang @CedricChampeau Traits : the basics • Override like

    any other interface • Compatible with @CompileStatic 20 @CompileStatic class Duck implements Flying, Quacking { @Override void fly() { println "Duck flying!" quack() } }
  18. #s2gx #groovylang @CedricChampeau Traits : the basics • Stateful 21

    trait Geolocalized { double longitude double latitude String placeName } class City implements Geolocalized {} def city = new City(longitude: 32.803468, latitude: -96.769879, placeName: 'Dallas')
  19. #s2gx #groovylang @CedricChampeau Traits : the basics • Multiple stateful

    traits allowed! • Major difference with inheritance 22 trait Inhabited { int population } class City implements Geolocalized, Inhabited {} def city = new City( longitude: 32.803468, latitude: -96.769879, placeName: 'Dallas', population: 1_280_000 ) println city.population
  20. #s2gx #groovylang @CedricChampeau Traits : first insight... • Traits can

    be designed as small bricks of behavior • Classes can be composed with traits • Increases reusability of components 23
  21. #s2gx #groovylang @CedricChampeau Traits : beyond basics • A trait

    may inherit from another trait 24 trait Named { String name } trait Geolocalized extends Named { long latitude long longitude } class City implements Geolocalized {}
  22. #s2gx #groovylang @CedricChampeau Traits : beyond basics • A trait

    may inherit from multiple traits 25 trait WithId { Long id } trait Place implements Geolocalized, WithId {} class City implements Place {}
  23. #s2gx #groovylang @CedricChampeau Traits : beyond basics • A trait

    can define abstract methods 26 trait Polite { abstract String getName() void thank() { println "Thank you, my name is $name" } } class Person implements Polite { String name } def p = new Person(name: 'Rob') p.thank()
  24. #s2gx #groovylang @CedricChampeau Traits : beyond basics • Why implements?

    • From Java • a trait is only visible as an interface • default methods of traits are not default methods of interface • From Groovy • A trait can be seen as an interface on steroids • A trait without default implementation is nothing but an interface 27
  25. #s2gx #groovylang @CedricChampeau Traits : conflicts • What if two

    traits define the same method ? 28 trait Worker { void run() { println "I'm a worker!" } } trait Runner { void run() { println "I'm a runner" } } class WhoAmI implements Worker, Runner {}
  26. #s2gx #groovylang @CedricChampeau Traits : conflicts • Conflict resolution rule:

    last trait wins 29 class WhoAmI implements Worker, Runner {} def o = new WhoAmI() o.run() // prints "runner" class WhoAmI implements Runner, Worker {} def o = new WhoAmI() o.run() // prints "worker"
  27. #s2gx #groovylang @CedricChampeau Traits : explicit conflict resolution • You

    can choose which method to call 30 trait Lucky { String adj() { 'lucky'} String name() { 'Alice' } } trait Unlucky { String adj() { 'unlucky'} String name() { 'Bob' } } class LuckyBob implements Lucky, Unlucky { String adj() { Lucky.super.adj() // we select the "adj" implementation from Lucky } String name() { Unlucky.super.name() // and the "name" implementation from Unlucky } String toString() { "${adj()} ${name()}"} } def l = new LuckyBob() assert l.toString() == 'lucky Bob'
  28. #s2gx #groovylang @CedricChampeau Traits : dynamic too! • Groovy is

    a dynamic language, so traits should be too! 31 trait ExpandoTrait { private final Map<String,?> values = [:] def propertyMissing(String name) { values.get(name) } void setProperty(String name, value) { println "Setting dynamic property $name" values.put(name, value) } } class Person implements ExpandoTrait { String name } def p = new Person(name: 'Cedric') assert p.name == 'Cedric' p.age = 35 assert p.age == 35
  29. #s2gx #groovylang @CedricChampeau Traits : dynamic too! • Traits can

    call methods which are defined in the implementing class 32 class A { void foo() { println 'A' } } class B { } trait CallsFooIfAvailable { void bar() { if (metaClass.getMetaMethod('foo')) { foo() } } } class SubA extends A implements CallsFooIfAvailable {} class SubB extends B implements CallsFooIfAvailable {} [new SubA(), new SubB()]*.bar()
  30. #s2gx #groovylang @CedricChampeau Traits : what's this? • “this” represents

    the trait'ed class 33 trait WhoIsThis { def self() { this } } class Me implements WhoIsThis {} def me = new Me() assert me.is(me.self())
  31. #s2gx #groovylang @CedricChampeau Traits : runtime coercion • Groovy way

    of implementing interfaces 34 interface MyListener { void onEvent(String name) } def explicit = { println it } as MyListener MyListener implicit = { println it } explicit.onEvent 'Event 1' implicit.onEvent 'Event 2'
  32. #s2gx #groovylang @CedricChampeau Traits : runtime coercion • Same can

    be done with traits 35 trait Greeter { abstract String getName() void greet() { println "Hello $name" } } def greeter = { 'Dan' } as Greeter Greeter greeter2 = { 'Dan' } greeter.greet() greeter2.greet()
  33. #s2gx #groovylang @CedricChampeau Traits : runtime coercion • Support for

    multiple traits at runtime 36 class PoorObject { void doSomething() { println "I can't do much" } } trait SuperPower { void superPower() { println 'But I have superpowers!' } } trait Named { String name } def o = new PoorObject() def named = o as Named named.name = 'Bob' // now can set a name named.doSomething() // and still call doSomething def powered = o as SuperPower powered.superPower() // now it has superpowers powered.doSomething() // it can do something def superHero = o.withTraits(SuperPower, Named) // multiple traits at once! superHero.name = 'SuperBob' // then you can set a name superHero.doSomething() // still call doSomething superHero.superPower() // but you have super powers too!
  34. #s2gx #groovylang @CedricChampeau Traits : runtime coercion gotchas • Coercion

    occurs at runtime, at call site • Returned object implements all interfaces and traits • But doesn't extend the original class! 37 class Person { String name } trait Flying { void fly() { println "$name flying!" } } def p = new Person(name: 'Thor') def f = p as Flying assert !p.is(f) // not the same instance assert f instanceof Flying // it's a Flying object assert !(f instanceof Person) // but it's not a Person anymore! println f.name // yet, name still exists f.fly()
  35. #s2gx #groovylang @CedricChampeau Traits : who's fastest? • Courtesy of

    Erik Pragt (@epragt) 38 trait NoStackTrace { Throwable fillInStackTrace() { return this } } class ExpensiveException extends RuntimeException { } class CheapException extends RuntimeException implements NoStackTrace {} def e1 = new CheapException() def e2 = new ExpensiveException() def e3 = new ExpensiveException() as NoStackTrace
  36. #s2gx #groovylang @CedricChampeau Traits : who's fastest? • Courtesy of

    Erik Pragt (@epragt) 39 user system cpu real Cheap Exception 42 0 42 43 Expensive Exception 6722 3 6725 6734 Make expensive exception cheap 14982 7 14989 15010 See https://gist.github.com/bodiam/48a06dc9c74a90dfba8c
  37. #s2gx #groovylang @CedricChampeau Traits : not virtual • Methods from

    traits are “copied” where applied 40 trait HasColor { String getColor() { 'red' } } class Colored implements HasColor {} def c = new Colored() assert c.color == 'red' class Colored2 implements HasColor { String getColor() { 'blue' } } def c2 = new Colored2() assert c2.color == 'blue' Takes precedence
  38. #s2gx #groovylang @CedricChampeau Traits : not virtual • Methods from

    traits are “copied” where applied 41 class Colored3 { String getColor() { 'yellow' } } class Colored4 extends Colored3 implements HasColor {} def c4 = new Colored4() assert c4.color == 'red' Takes precedence • Traits can be used to share overrides!
  39. #s2gx #groovylang @CedricChampeau Traits : the meaning of “super” •

    super has a special meaning in traits 42 Lucky.super.foo() Qualified super • But super can also be used for chaining traits interface MessageHandler { void onMessage(String tag, Map<?,?> payload) } trait DefaultMessageHandler implements MessageHandler { void onMessage(String tag, Map<?, ?> payload) { println "Received message [$tag]" } }
  40. #s2gx #groovylang @CedricChampeau Traits : the meaning of “super” class

    MyHandler implements DefaultMessageHandler {} def h = new MyHandler() h.onMessage("event",[:])
  41. #s2gx #groovylang @CedricChampeau Traits : the meaning of “super” //

    A message handler which logs the message and // passes the message to the next handler in the chain trait ObserverHandler implements MessageHandler { void onMessage(String tag, Map<?, ?> payload) { println "I observed a message: $tag" if (payload) { println "Payload: $payload" } // pass to next handler in the chain super.onMessage(tag, payload) } } class MyNewHandler implements DefaultMessageHandler, ObserverHandler {} def h2 = new MyNewHandler() h2.onMessage('event 2', [pass:true])
  42. #s2gx #groovylang @CedricChampeau Traits : chaining with runtime coercion class

    BaseHandler implements MessageHandler { @Override void onMessage(final String tag, final Map<?, ?> payload) { println "Received [$tag] from base handler" } } trait UpperCaseHandler implements MessageHandler { void onMessage(String tag, Map<?, ?> payload) { super.onMessage(tag.toUpperCase(), payload) } } trait DuplicatingHandler implements MessageHandler { void onMessage(String tag, Map<?, ?> payload) { super.onMessage(tag, payload) super.onMessage(tag, payload) } } def h = new BaseHandler() h = h.withTraits(UpperCaseHandler, DuplicatingHandler, ObserverHandler) h.onMessage('runtime', [:])
  43. #s2gx #groovylang @CedricChampeau Traits : inheritance of state gotchas •

    Backing fields are per trait 47 trait IntCouple { int x = 1 int y = 2 int sum() { x+y } } class BaseElem implements IntCouple { int f() { sum() } } def base = new BaseElem() assert base.f() == 3
  44. #s2gx #groovylang @CedricChampeau Traits : inheritance of state gotchas •

    Backing fields are per trait 48 class Elem implements IntCouple { int x = 3 int y = 4 int f() { sum() } } def elem = new Elem() println elem.f() What do you think?
  45. #s2gx #groovylang @CedricChampeau Traits : inheritance of state gotchas •

    Backing fields are per trait 49 trait IntCouple { int x = 1 int y = 2 int sum() { getX()+getY() } } class Elem implements IntCouple { int x = 3 int y = 4 int f() { sum() } } def elem = new Elem() assert elem.f() == 7
  46. #s2gx #groovylang @CedricChampeau Traits : inheritance of state gotchas •

    Avoids the diamond problem 50 class BaseElem implements IntCouple {} BaseElem.declaredFields.each { println it.name } // outputs gotchas_IntCouple__x gotchas_IntCouple__y Fields are “namespaced”
  47. #s2gx #groovylang @CedricChampeau Traits : AST transformations 52 • Unless

    stated differently, consider AST xforms incompatible • Both for technical and logical reasons • Is the AST xform applied on the trait or should it be applied when the trait is applied?
  48. #s2gx #groovylang @CedricChampeau Traits : AST transformations 53 • This

    works @CompileStatic trait StaticallyCompiledTrait { int x int y int sum() { x + y } }
  49. #s2gx #groovylang @CedricChampeau Traits : AST transformations 54 • This

    works too trait Delegating { @Delegate Map values = [:] } class Dynamic implements Delegating {} new Dynamic().put('a','b')
  50. #s2gx #groovylang @CedricChampeau Traits : AST transformations 55 • But

    this doesn't trait Named { String name } @ToString // ToString misses the "name" property class Foo implements Named {}
  51. #s2gx #groovylang @CedricChampeau Traits : rethinking API design conclusion 57

    • Traits let you think about components • Traits can be seen as generalized delegation • Methods can accept traits as arguments • Traits are stackable • Traits are visible as interfaces from Java
  52. #s2gx #groovylang @CedricChampeau Traits : API independent extension with DSL

    58 • DSL can be used to augment an API • No need to subclass the base API trait ListOfStrings implements List<String> { int totalLength() { sum { it.length() } } } trait FilteringList implements List<String> { ListOfStrings filter() { findAll { it.contains('G') || it.contains('J') } as ListOfStrings } } def list = ['Groovy', 'rocks', 'the', 'JVM'] println list.withTraits(FilteringList).filter().totalLength()
  53. #s2gx #groovylang @CedricChampeau Conclusion 59 • Traits extend the benefit

    of interfaces to concrete classes • Traits do not suffer the diamond problem • Traits allow a new set of API to be written • APIs can be augmented without modifications or extension http://docs.groovy-lang.org/2.3.6/html/documentation/core-traits.html