A trait is a behavior that can be mixed into or assimilated into a class hierarchy. For example, to model a friend abstraction, we can mix a Friend trait into any class—Man, Woman, Dog, and so on—without having to inherit all those classes from a common base class.
To understand the benefits of traits, let’s design an example first without them. We’ll start with a class Human and make it friendly. In the simplest form, a friend is someone who listens. To support this abstraction, here is the listen method that we’d add to the Human class:
| class Human(val name: String) { |
| def listen() = println(s"Your friend $name is listening") |
| } |
| |
| class Man(override val name: String) extends Human(name) |
| class Woman(override val name: String) extends Human(name) |
One disadvantage in this code is the friendly quality does not quite stand out and is merged into the Human class. Furthermore, a few weeks into development, we realize we forgot man’s best friend. Dogs are great friends—they listen to us quietly when we have a lot to unload. But, it’s hard to make a Dog a friend in the current design. We can’t inherit a Dog from a Human for that purpose.
This is where Scala’s traits come in. A trait is like an interface with a partial implementation. The vals and vars we define and initialize in a trait get internally implemented in the classes that mix the trait in. Any vals and vars defined but not initialized are considered abstract, and the classes that mix in these traits are required to implement them. We can reimplement the Friend concept as a trait:
UsingTraits/Friend.scala | |
| trait Friend { |
| val name: String |
| def listen() = println(s"Your friend $name is listening") |
| } |
Here we have defined Friend as a trait. It has a val named name that is treated as abstract. We also have the implementation of a listen method. The actual definition or the implementation of name will be provided by the class that mixes in this trait. Let’s look at ways to mix in the trait:
UsingTraits/Human.scala | |
| class Human(val name: String) extends Friend |
| |
| class Woman(override val name: String) extends Human(name) |
| class Man(override val name: String) extends Human(name) |
The class Human mixes in the Friend trait. If a class does not extend from any other class, then use the extends keyword to mix in the trait. The class Human and its derived classes Man and Woman simply use the implementation of the listen method provided in the trait. We can override this implementation if we like, as you’ll see soon.
We can mix in any number of traits. To mix in additional traits, use the keyword with. We’ll also use the keyword with to mix in the first trait if a class already extends from another class like the Dog in this next example. In addition to mixing in the trait, we have overridden its listen method in Dog.
UsingTraits/Dog.scala | |
| class Dog(val name: String) extends Animal with Friend { |
| //optionally override method here. |
| override def listen() = println(s"$name's listening quietly") |
| } |
The base class of Dog is Animal, defined separately here:
UsingTraits/Animal.scala | |
| class Animal |
We can call the methods of a trait on the instances of classes that mix it in, and also treat a reference to such classes as a reference of the trait:
UsingTraits/UseFriend.scala | |
| object UseFriend extends App { |
| val john = new Man("John") |
| val sara = new Woman("Sara") |
| val comet = new Dog("Comet") |
| |
| john.listen |
| sara.listen |
| comet.listen |
| |
| val mansBestFriend : Friend = comet |
| mansBestFriend.listen |
| |
| def helpAsFriend(friend: Friend) = friend.listen |
| |
| helpAsFriend(sara) |
| helpAsFriend(comet) |
| } |
Here’s the output from the previous code:
| Your friend John is listening |
| Your friend Sara is listening |
| Comet's listening quietly |
| Comet's listening quietly |
| Your friend Sara is listening |
| Comet's listening quietly |
Traits look similar to classes but have some significant differences. First, they require the mixed-in class to implement the uninitialized (abstract) variables and values declared in them. Second, their constructors cannot take any parameters. Traits are compiled into Java interfaces with corresponding implementation classes that hold any methods implemented in the traits.
Traits do not suffer from the method collision problem that generally arises from multiple inheritance. They avoid it by late binding with the method of the class that mixes them in. A call to super within a trait resolves to a method on another trait or the class that mixes it in, as you’ll soon see.
Mixing traits is not limited to classes. We can mix them into instances as well, as you’ll learn next.
3.138.169.40