Save 37% off PRO during our Black Friday Sale! »

Rethinking API design with traits

Rethinking API design with traits

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

F929f5d80ef8a23b67ad8ac6f08416cd?s=128

Cédric Champeau

September 09, 2014
Tweet

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 Social (or why to use #groovylang) 3

  4. #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
  5. Life without traits (or why it's primarily a static issue)

    5
  6. #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
  7. #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
  8. #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()
  9. #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
  10. #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
  11. #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'
  12. #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
  13. #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
  14. #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
  15. #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 { }
  16. #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
  17. Traits in Groovy 17

  18. #s2gx #groovylang @CedricChampeau Traits • Similar to Java 8 default

    methods • Supported in JDK 6, 7 and 8 • Stateful • Composable • Not really virtual... 18
  19. #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 {}
  20. #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() } }
  21. #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')
  22. #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
  23. #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
  24. #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 {}
  25. #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 {}
  26. #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()
  27. #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
  28. #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 {}
  29. #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"
  30. #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'
  31. #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
  32. #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()
  33. #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())
  34. #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'
  35. #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()
  36. #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!
  37. #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()
  38. #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
  39. #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
  40. #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
  41. #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!
  42. #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]" } }
  43. #s2gx #groovylang @CedricChampeau Traits : the meaning of “super” class

    MyHandler implements DefaultMessageHandler {} def h = new MyHandler() h.onMessage("event",[:])
  44. #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])
  45. #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', [:])
  46. Gotchas 46

  47. #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
  48. #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?
  49. #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
  50. #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”
  51. #s2gx #groovylang @CedricChampeau Traits : AST transformations 51

  52. #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?
  53. #s2gx #groovylang @CedricChampeau Traits : AST transformations 53 • This

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

    works too trait Delegating { @Delegate Map values = [:] } class Dynamic implements Delegating {} new Dynamic().put('a','b')
  55. #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 {}
  56. API design? 56

  57. #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
  58. #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()
  59. #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
  60. 60 @CedricChampeau melix http://melix.github.io/blog