You are mixing up some concepts that are relatively orthogonal to each other even though they all allow you some form interaction with Scala's OO model, namely:
trait
s and abstract class
es
- abstract
def
and val
s
- the
override
qualifier
You may notice that the compiler does not forbid you, for your particular example, to interchangeably use a trait
or an abstract class
, as well as adding and removing the override
qualifier whether the concrete class implements a trait
or an abstract class
. The only thing you cannot do is implementing a def
out of a non-concrete base class which defines the member as a def
(more on that later).
Let's go through these concepts one at a time.
trait
s and abstract class
es
One of the main differences between trait
s and abstract class
es is that the former can be used for multiple inheritance. You can inherit from one or more trait
s, an abstract class
and one or more trait
s or one abstract class
.
The other is that abstract class
es can have constructor parameters, which makes them more interesting to handle dynamic construction of objects with parameters that are only available at runtime.
You should reason about these characteristics when deciding which one to use them. In the DI use case, since you may want to establish a self type annotation that refers to more than one type (e.g.: self => Trait1 with Trait2
), trait
s tend to give you more freedom and are generally favored.
It may be worth noting at this point that historically abstract class
es tended to interact better with the JVM (as they had an analogous construct Java) and that in Scala 3 (currently under development under the name Dotty) trait
s will have the possibility to get constructor parameters, making them more powerful then abstract class
es.
abstract def
s and val
s
It is always suggested that you define abstract members as def
s, as this leaves you the freedom to define it as either a def
or a val
in concrete implementations.
The following is legal:
trait Trait {
def member: String
}
class Class extends Trait {
val member = "Class"
}
The following doesn't compile:
trait Trait {
val member: String
}
class Class extends Trait {
def member = "Class"
}
the override
qualifier
Using override
is highly suggested precisely because of the reason you mentioned, but please note that this is not bound to using val
. In fact, the following is legal code:
trait Trait {
val member: String
}
class Class extends Trait {
val member = "Class"
}
The override
qualifier is mandatory only if the extended class
or trait
already provides a concrete implementation of the particular field or method you are overriding. The following code does not compile and the compiler will tell you that the override
keyword is mandatory:
trait Trait {
def member = "Trait"
}
class Class extends Trait {
val member = "Class"
}
But as I already mentioned and as you already noted, the override
keyword is very useful to clarify what is being overridden and what is not, so it is highly suggested that you use it even when it's not strictly necessary.
Bonus of val
s and def
s
Another good reason to not use val
s in trait
s is to prevent making the order in which their are inherited meaningful, that in real-life scenarios can lead to some tough head-scratcher when trait
s also provide concrete implementations.
Consider the following code (that you can also run and play with here on Scastie):
trait HasTrunk {
val trunk = {
println("I have a trunk")
"trunk"
}
}
trait HasLegs {
val legs = {
println("I have legs")
"legs"
}
}
final class Elefant extends HasTrunk with HasLegs
final class Fly extends HasLegs with HasTrunk
new Elefant
new Fly
Since val
s must be evaluated at construction time, their side-effects are too: notice how the construction behavior changes, with operations performed at different times, even though the two classes extend the same trait
s.