En scala, los métodos de los Trais pueden tener implementaciones. Por ejemplo:
trait ConsoleLogger {
def log(msg: String) { println(msg) }
}
ConsoleLogger provee una implementación, en este caso imprime el log en consola.
Si deseamos utilizar este trait:
class SavingsAccount extends Account with ConsoleLogger {
def withdraw(amount: Double) {
if (amount > balance) log("Insufficient funds")
else balance -= amount
}
…
}
Como se puede ver SavingsAccount es una clase concreta y utiliza el método por defecto de ConsoleLogger. En Java 8, también se puede realizar esto dado que se agregaron los métodos por defecto en las interfaces. Pero en scala una interfaz puede tener estado cosa que no se puede hacer en java.
En scala (y también en otros lenguajes que permiten esto) decimos que la función de ConsoleLog es “mixed in” con la clase SavingsAccount.
Objetos con Trait
Se puede agregar un Trait con una implementación determinada a un objeto cuando se lo instancia. Veamos un ejemplo:
abstract class SavingsAccount extends Account with Logger {
def withdraw(amount: Double) {
if (amount > balance) log("Insufficient funds")
else …
}
...
}
Esta clase es abstracta no tiene implementación para el método log por lo tanto o no podríamos instanciarla o (en java por ejemplo) podemos implementar esto en una clase anónima pero en scala tenemos otra opción podemos agregar una interfaz que implemente este método con la palabra with :
val acct = new SavingsAccount with ConsoleLogger
Por supuesto podemos llamar a otra instancia con diferente traits:
val acct2 = new SavingsAccount with FileLogger
Traits en capas
Puede agregar, a una clase o a un objeto, múltiples rasgos que se invocan entre sí a partir de la última llamada. Esto es útil cuando se necesita transformar un valor en etapas.
Por ejemplo:
trait TimestampLogger extends ConsoleLogger {
override def log(msg: String) {
super.log(s"${java.time.Instant.now()} $msg")
}
}
Supongamos que queremos acotar el mensaje:
trait ShortLogger extends ConsoleLogger {
override def log(msg: String) {
super.log(if (msg.length <= 15) msg else s"${msg.substring(0, 12)}...")
}
}
Como puede ver cada método log es modificado en las traits . La expresión super.log no tiene el mismo significado que en las clases. En los traits la expresión super.log no llama al método del padre sino que importa el orden con que se han agregado. Veamos 2 ejemplos:
val acct1 = new SavingsAccount with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount with ShortLogger with TimestampLogger
Si utilizamos acct1 el log sera:
Sun Feb 06 17:45:45 ICT 2011 Insufficient...
Como podemos ver primero recorta el texto y luego agrega la fecha y si utilizamos acct2 :
Sun Feb 06 1...
Si utilizamos acct2 primero agrega la fecha y luego recorta el texto, con lo que no nos imprime ni siquiera la fecha completa.
Note que se puede indicar con super[ConsoleLog].log(...) de que padre vamos a llamar a este método.