Reputation: 693
In Scala dependency injection with type annotation, the injected class/object reference can be either implemented as a def trait member or val abstract member, like:
trait InjectedTrait {}
class InjectedClass extends InjectedTrait {}
trait TestTrait {
def injectedTrait: InjectedTrait
}
class TestClass {
this: TestTrait =>
}
// In main()
val obj = new TestClass() with TestTrait {
val injectedTrait = new InjectedClass()
}
or
abstract class AbstractInjectedClass {}
class InjectedClass extends AbstractInjectedClass {}
trait TestTrait {
val injectedClass: AbstractInjectedClass
}
class TestClass {
this: TestTrait =>
}
// In main()
val obj = new TestClass() with TestTrait {
override val injectedClass = new InjectedClass()
}
Any reasons you would prefer one over the other? - I personally like the second one because the 'override' keyword clearly expresses what's happening.
Upvotes: 1
Views: 925
Reputation: 12804
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
esdef
and val
soverride
qualifierYou 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
esOne 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.
def
s and val
sIt 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"
}
override
qualifierUsing 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.
val
s and def
sAnother 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.
Upvotes: 0