1
votes

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.

1
What if you want to extend more then one class? I mean multiple inheritance like class a extends b then what? That’s one of the reason we have traits.Raman Mishra
Well that's true. But in DI I don't think I'd want to have many mixins for the class objects injected. That just makes things messy rather than having a clearer inheritance hierarchy. I would rather build another class object to inject.peidaqi

1 Answers

0
votes

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:

  • traits and abstract classes
  • abstract def and vals
  • 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.

traits and abstract classes

One of the main differences between traits and abstract classes is that the former can be used for multiple inheritance. You can inherit from one or more traits, an abstract class and one or more traits or one abstract class.

The other is that abstract classes 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), traits tend to give you more freedom and are generally favored.

It may be worth noting at this point that historically abstract classes 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) traits will have the possibility to get constructor parameters, making them more powerful then abstract classes.

abstract defs and vals

It is always suggested that you define abstract members as defs, 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 vals and defs

Another good reason to not use vals in traits 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 traits 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 vals 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 traits.