1. Introduction
In object-oriented programming, behaviour can be passed from one type of object to another using inheritance. Simple inheritance works very well for many problems, but there are situations in which inheritance alone is not sufficient or practical.
In this tutorial, we’ll explore using traits during the instantiation of objects as a powerful alternative to inheritance.
2. Understanding the Problem
Let’s imagine the case of Person, with basic attributes that everyone has (name, address, dateOfBirth), and basic functionality that makes sense for everyone (breathe, eat, walk, say).
A hierarchy can be created to represent special types of Person. A Musician, for example, with additional attributes like instrument, genre and repertoire, and additional functionality like tuneInstrument and playSong. Or a Politician, with attributes like politicalParty, politicalOrientation or charge, and functionality like vote or saySpeech.
How do we represent someone that is a Politician and a Musician simultaneously?
- Simple inheritance doesn’t help much with that. One possible alternative approach is multiple inheritance (as is done in C++) – we could create a class PoliticalMusician, for example, inheriting from both classes Politician and Musician. Experience has shown, though, that this creates new problems (say that politicians and musicians have different ideas of how to shout and why. So both classes implement a method shout(). Which interpretation will our PoliticalMusician follow? This ambiguity is the infamous diamond problem).
- Some languages use interfaces (Java is perhaps the most famous case). They are merely contracts. Any class that implements that contract is obliged by the compiler to provide implementations of the methods declared.
- Other languages, our Scala included, offer traits. Traits are similar in concept to interfaces, but they additionally allow us to provide default implementations of the methods.
With traits, we don’t have to pollute our hierarchy of classes when all we want to do is give certain abilities to our objects(the traits they have) without changing what they are(the class they belong to).
3. Description of the Solution
The idea of abstracting behaviour in traits is extremely versatile because to exploit the power of traits, we don’t have to limit ourselves to the declaration of classes! Instead of adding traits to the declaration of classes, we’ll mix them when we declare instances.
This approach gives us some mix-and-match functionality. For that reason, this technique is also called Scala mix-ins.
In coding terms, we can create our persons as usual – create instances of the class Person, and then mix in whichever abilities a given person has when we create them! For example, let Pat be just a Person:
class Person(name: String, address: String, dateOfBirth: LocalDate) {
def breathe(): String = s"I'm $name, alive and kicking"
def eat(): String = "I'm eating: chomp chomp"
def walk(): String = "I'm walking: away I go"
def saySomething(what: String): String = s"I say: $what"
}
val pat: Person =
new Person("Pat", "123 Main St.", LocalDate.of(1933, 10, 11))
Let Mary (who is, of course, a Person) also be a Musician:
trait Musician {
val instrument: String
def tuneInstrument(): String =
s"I'm tuning my $instrument"
def playSong(song: String): String =
s"I'm playing the beautiful song $song"
}
val mary: Person with Musician =
new Person("Mary", "456 Second St.", LocalDate.of(1982, 9, 9))
with Musician { override val instrument: String = "guitar" }
We should note is important that the type of mary is not Person. Instead, it’s Person with Musician.
Let Prudence be a Politician:
trait Politician {
def vote(): String =
"I'm voting"
def saySpeech(speech: String): String =
s"I'm saying an important speech: $speech"
}
val prudence: Person with Politician =
new Person("Prudence", "789 Third St.", LocalDate.of(1972, 6, 3))
with Politician
As before, the type of prudence isn’t Person. It’s Person with Politician.
Now let Giorgio be both:
val giorgio: Person with Politician with Musician =
new Person("Giorgio", "121 Fourth St.", LocalDate.of(1980, 2, 19))
with Politician
with Musician {
override val instrument: String = "flute"
}
Surely we surmise already that the type of Giorgio is Person with Politician with Musician.
Although a little unorthodox, we can also have an elephant called Ellie that plays the trombone:
class Animal(name: String, species: String) {
def makeNoise(noise: String): String =
s"I'm $name (a $species) making this noise: $noise"
}
val ellie: Animal with Musician =
new Animal("Ellie", "elephant") with Musician {
override val instrument: String = "trombone"
}
We can also have a monkey Vasily that does politics:
val vasily: Animal with Politician =
new Animal("Vasily", "monkey") with Politician
4. Testing the Solution
Using ScalaTest, let’s test the above:
"A person" should {
"know how to breathe and say their name" in {
assertResult("I'm Pat, alive and kicking")(pat.breathe())
}
}
We are checking here just the basics – that any person should be able to breathe and say their own name. Let’s write another test:
"A musician" should {
"know their name, as a person" in {
assertResult("I'm Mary, alive and kicking")(mary.breathe())
}
"tune their instrument" in {
assertResult("I'm tuning my guitar")(mary.tuneInstrument())
}
}
Mary is not only a person, but she’s also a musician. So she can say her name, but she can also tune her guitar. Let’s do one more:
"A politician" should {
"know their name, as a person" in {
assertResult("I'm Prudence, alive and kicking")(prudence.breathe())
}
"say an important speech, as a politician" in {
assertResult("I'm saying an important speech: blah, blah, blah...")(
prudence.saySpeech("blah, blah, blah...")
)
}
}
Something similar happens with Prudence. She’s a person just as well, but she’s also a politician. Therefore, she can say important speeches.
A slightly more interesting case is Giorgio’s because he’s a person that does politics but also plays the flute:
"A musician/politician" should {
"know their name, as a person" in {
assertResult("I'm Giorgio, alive and kicking")(giorgio.breathe())
}
"cast a vote, as a politician" in {
assertResult("I'm voting")(giorgio.vote())
}
"play a song" in {
assertResult("I'm playing the beautiful song #1")(giorgio.playSong("#1"))
}
}
5. Conclusion
In this article, we’ve looked at a technique to enrich the behaviour of objects, adding traits to them at the moment they are declared. The power we get by doing that is considerable and, at the same time, very convenient.
As usual, the full code for this article is available over on GitHub.