Inc. and licensed under a Creative Commons Attribution-NonCommercial license: http://creativecommons.org/licenses/by-nc/3. Rethinking API design with traits By Cédric Champeau Rethinking API design with traits in Groovy by Cédric Champeau
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
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'
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
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
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
trait Geolocalized { double longitude double latitude String placeName } class City implements Geolocalized {} def city = new City(longitude: 32.803468, latitude: -96.769879, placeName: 'Dallas')
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
may inherit from another trait 24 trait Named { String name } trait Geolocalized extends Named { long latitude long longitude } class City implements Geolocalized {}
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()
• 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
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"
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!
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
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!
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]" } }
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])
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?
• 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 • 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()
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.7/html/documentation/core-traits.html