· Scala doesn't have interfaces. Instead it has something similar called traits.
· Like Java interfaces, Scala traits can't be instantiated.
· Like Java classes, traits can have concrete methods and fields.
Types of traits
· interfaces: all methods are abstract
· mix-ins: all methods are concrete
· rich interfaces: concrete and abstract methods present.
Use traits the same way you would use interfaces. In the example below a warrior in a videogame attacks other warriors using a dynamically changeable attack strategy. An attack strategy is anything that implements the Attack interface (trait):
class Warrior(val name: String):
var health = 100
var strategy: Attack = null
def damage(damageAmt: Int): Unit =
health = math.max(0, health
- damageAmt)
def attack(opponent: Warrior): Unit =
println(name + " is
attacking " + opponent.name)
strategy.attack(opponent)
trait Attack: // trait used as an interface
def attack(target: Warrior): Unit
class Stab extends Attack:
val damageAmount = 10
def attack(target: Warrior): Unit =
println(" stabbling " + target.name)
// more stabbing behavior goes here
target.damage(damageAmount)
println(" " + target.name + " health = "
+ target.health)
class Poison extends Attack:
val damageAmount = 5
def attack(target: Warrior): Unit =
println(" poisoning " + target.name)
// more poisoning behavior goes here
target.damage(damageAmount)
println(" " + target.name + " health = "
+ target.health)
// Stomp, CastSpell, Zap, etc.
Test Harness:
object tournament extends App:
val a =
Warrior("Dink")
val b =
Warrior("Donk")
a.strategy = Stab()
b.strategy = Poison()
a.attack(b)
b.attack(a)
b.strategy = Stab()
b.attack(a)
Output:
Dink is attacking Donk
stabbling Donk
Donk health = 90
Donk is attacking Dink
poisoning Dink
Dink health = 95
Donk is attacking Dink
stabbling Dink
Dink health = 85
Notes:
· A trait declaration looks like a class declaration but with the reserved word "trait".
· This is an example of the Strategy Design Pattern.
Traits can have concrete fields and methods that are inherited by the classes and singletons that extend them.
In this version of our video game we have done away with the Attack interface. Attack strategies can call their attacking methods by any name.
trait Stab:
val stabDamage = 10
def stab(target: Warrior) =
println(" stabbling " + target.name)
// etc.
target.damage(stabDamage)
println(" " + target.name + " health = "
+ target.health)
trait Poison:
val poisonDamage = 5
def poison(target: Warrior) =
println(" poisoning " + target.name)
// etc.
target.damage(poisonDamage)
println(" " + target.name + " health = "
+ target.health)
We can combine traits using "with" as in:
trait PoisonAndStab extends Poison with Stab
Classes can extend these combined traits:
class Warrior(val name: String) extends Stab with Poison:
var health = 100
def damage(damageAmt: Int): Unit = health
=
math.max(0, health -
damageAmt)
def attack(opponent: Warrior): Unit =
println(name + " is
attacking " + opponent.name)
stab(opponent)
poison(opponent)
stab(opponent)
Notes:
· Think of "with" as an operator that combines two traits into a new single trait.
· It's probably best not to think of classes implementing traits (although as we have seen interface is a role traits can play). It's better to say classes and singletons inherit from traits. Some of the methods they inherit may be abstract and need to be implemented.
· Another way to think about a class extending a trait is that we are adding properties (fields) and behaviors (methods) to the class the same way that some ice cream parlors will mix-in chocolate chips, gummies, and other goodies into your vanilla ice cream scoop. We say that Stab and Poisons are mix-ins for Warrior.
In earlier times it was possible to inherit your deceased uncle's debts. This is what it's like for a class that implements an interface. The class is obligated to implement the abstract methods (debts) inherited from interfaces. But wait, with traits that deadbeat uncle might have had a mattress stuffed with money. In other words we inherit abstract methods and concrete methods (i.e., mattresses stuffed with money.) We can think of traits as rich interfaces.
In this version of our game we re-introduce the Attack trait, but factor some of the common behavior of all attacking behavior into a concrete template method in the Attack trait. Details of the attack specific behavior (poisoning, stabbing, etc.) are specified by implementing an abstract details method:
trait Attack:
val damageAmount: Int // abstract
field
def details(target: Warrior): Unit
def attack(target: Warrior): Unit =
details(target)
target.damage(damageAmount)
println(" " + target.name + " health = "
+ target.health)
class Stab extends Attack:
val damageAmount = 10
def details(target: Warrior): Unit =
println(" stabbling " + target.name)
// etc.
class Poison extends Attack:
val damageAmount = 5
def details(target: Warrior): Unit =
println(" poisoning " + target.name)
// etc.
Notes:
· damageAmount and details can be declared protected.
· This is an example of the Template Design Pattern. A concrete template method calls abstract detail methods. This is useful when the high-level behavior of a method is known, but the low-level details are not.
Of course singletons can extend traits, too.
class Warrior(val name: String):
var health = 100
def damage(damageAmt: Int): Unit =
health = math.max(0, health
- damageAmt)
trait Poison:
val poisonDamage = 5
def poison(target: Warrior): Unit =
println(" poisoning " + target.name)
// etc.
target.damage(poisonDamage)
println(" " + target.name + " health = "
+ target.health)
trait Stab:
val stabDamage = 10
def stab(target: Warrior): Unit =
println(" stabbling " + target.name)
// etc.
target.damage(stabDamage)
println(" " + target.name + " health = "
+ target.health)
In our test code we introduce Dink and Donk, two people (i.e., objects) that can attack any opponent having warrior properties (name and health). Dink and Donk both have these properties. In addition, Dink has stabbing abilities he can mix-in to his attacks. Donk has stabbing and poisoning abilities.
object dink extends Warrior("Dink") with Stab:
def attack(opponent: Warrior): Unit =
println(name + " is attacking
" + opponent.name)
stab(opponent)
object donk extends Warrior("Donk") with Stab with
Poison:
def attack(opponent: Warrior): Unit =
println(name + " is attacking
" + opponent.name)
stab(opponent)
poison(opponent)
object Tournament extends App:
dink.attack(donk)
donk.attack(dink)
With traits calling super.method() has a slightly different meaning than it does with classes. Instead of calling an inherited method, it calls the method of another trait. More specifically, it follows the Back-to-Front rule that states that super always refers to the trait to the left in a sequence such as T1 with T2 with T3 ...
Here's an example: superDemo.scala.
From the output we can see several interesting things. First, the order of trait construction moves from right to left. Second, the order of method invocation moves from left to right.
The Back-to-Front rule is a useful way to build pipelines that process data in stages.
In this version of our game we make some radical changes. A warrior attacks his opponent by damaging him with different types of strikes: stabbing, poisoning, etc. Fortunately, the opponent can reduce the strength of an incoming strike with his shield.
class Strike(var strength: Int = 0)
class Stab extends Strike(20)
class Poison extends Strike(10)
// etc.
class Shield:
def reduceDamage(strike:
Strike): Strike =
println(" reducing
Strike")
strike.strength -= 1 // just a bit
strike
class Warrior(val name: String):
var shield: Shield = Shield()
var health: Int = 100
def damage(strike: Strike): Unit =
health
= health
- shield.reduceDamage(strike).strength
def attack(opponent: Warrior): Unit =
println(name + " stabbing "
+ opponent.name)
opponent.damage(Stab())
println(name + " poisoning
" + opponent.name)
opponent.damage(Poison())
The basic shield is pretty useless, but there are enhancements that can be added when a shield is created:
trait PoisonProtection extends
Shield:
override def reduceDamage(strike:
Strike): Strike =
if (strike.isInstanceOf[Poison])
println(" reducing
poison")
strike.strength -= 5
else
println(" unable to
reduce " + strike.getClass)
super.reduceDamage(strike)
trait StabProtection extends Shield:
override def reduceDamage(strike: Strike): Strike =
if (strike.isInstanceOf[Stab])
println(" reducing
stab")
strike.strength -= 3
else
println(" unable to
reduce " + strike.getClass)
super.reduceDamage(strike)
In our test code Dink's shield can reduce the strength of poisoning and stabbing Strikes. Donk's shield can only reduce the strength of stabbing Strikes:
object tournament extends App:
val dink
= new Warrior("Dink")
dink.shield = new Shield() with
PoisonProtection with StabProtection
val donk
= new Warrior("Donk")
donk.shield = new Shield() with
PoisonProtection
dink.attack(donk)
donk.attack(dink)
println("Dink health = " + dink.health)
println("Donk health = " + donk.health)
In the test code output we see that Donk reduced the strength of Dink's stab, his poison shield was useless, and his basic shield was almost useless. Dink was able to reduce the strength of Donk's poison and stab:
Dink stabbing Donk
unable to reduce class clash.Stab
reducing Strike
Dink poisoning Donk
reducing poison
reducing Strike
Donk stabbing Dink
reducing stab
unable to reduce class clash.Stab
reducing Strike
Donk poisoning Dink
unable to reduce class clash.Poison
reducing poison
reducing Strike
Dink health = 80
Donk health = 77
Notes:
· Traits can extend classes.
· When Donk poisoned Dink we first saw Dink's stab protection say that it was unable to reduce the poison. Next we saw the Dink's poison protection successfully reduce the strength of the Strike. Finally, we saw Dink's basic shield slightly reduce the Strike by an additional amount. This is the reverse of the order that the protections were added to Dink's shield.
· This is similar to the Decorator Pattern but with two major defects:
1. Shields can't be dynamically enhanced.
2. We can't add multiple layers of the same type of protection. For example, the following is forbidden:
donk.shield = new Shield() with PoisonProtection with PoisonProtection // error
· This is similar to the Delegation Pattern, where we create a chain of objects, each delegating to the next. Delegation chains are an alternative to inheritance. Instead of all instances of Shield inheriting the features of any super classes, different instances of Shield can pick which features they want to inherit.