13. trait Car {
def engine : Engine
def drive() {
engine.start()
println("Vroom vroom")
}
def park() {
if (engine.isRunning() ) println("Break!")
engine.stop()
}
}
val myCar = new Car {
override val engine = new DieselEngine()
}
14. Hmm OK, a Car has an Engine.
That's better.
But...
15. There is no guarantee that the
Engine in myCar isn't used in
another Car!
:(
16. If only there was a way to "mix-in" an
Engine into my car rather than supplying
it from the outside
Enter: self-types
17. âA self type of a trait is the assumed type of this,
the receiver, to be used within the trait. Any
concrete class that mixes in the trait must ensure
that its type conforms to the traitâs self type.â
Programming in Scala
22. â Self-types are used with traits
â Explicitly declare the type of the value this
â Specify the requirements on any concrete class
or instance the trait is mixed into.
â Declare a dependency of the trait on another
type: âIn order to use me you have to be one of
thoseâ
24. No need to call the self-type "this"
You can use this-aliasing to give it a
different name:
25. trait Car {
engine: Engine =>
def drive() {
engine.start()
println("Vroom vroom")
}
}
Useful for nested classes or traits, where accessing
a particular this would otherwise be difficult
26. Self-types donât automatically inherit:
trait HondaCar extends Car
// error: self-type HondaCar does not conform to Car's selftype Car
with Engine
Need to repeat the self-type in subtypes:
trait HondaCar extends Car {
this: Engine =>
// ...
}
27. A self-type can require multiple types:
trait Car {
this: Engine with FuelTank with GearBox =>
// ...
}
Used when the trait has multiple dependencies
28. The self-type can be a structural type:
trait Car {
this: {
def start: Unit
def stop: Unit
} =>
// ...
}
Allows for safe mixins with duck-typing.
(Useful when interacting with external dependencies)
30. Hmm, well no:
val myCar = new Car extends DieselEngine {}
myCar is still a Car and an Engine
:(
31. So here's a quick fix:
val myCar: Car = new Car extends DieselEngine {}
⊠but that's cheating (a bit)!
And it doesn't work for singletons:
object MyCar extends Car with DieselEngine
32. What we need is a way to wire up and assemble
our components without changing their identity
36. For each component in our system, supply a
Component trait, that declares:
â Any dependent components, using self-types
â A trait describing the component's interface
â An abstract val that will be instantiated with
an instance of the component
â Optionally, implementations of the
component interface
37. trait EngineComponent {
trait Engine {
private var running = false
def start(): Unit = { /* as before */ }
def stop(): Unit = {/* as before */ }
def isRunning: Boolean = running
def fuelType: FuelType
}
protected val engine : Engine
protected class DieselEngine extends Engine {
override val fuelType = FuelType.Diesel
}
}
38. trait CarComponent {
this: EngineComponent => // gives access to engine
trait Car {
def drive(): Unit
def park(): Unit
}
protected val car: Car
protected class HondaCar extends Car {
override def drive() {
engine.start()
println("Vroom vroom")
}
override def park() { ⊠}
}
}
39. Great, now let's tie it all together:
(remember a Car has a couple more components beside an Engine)
40. object App extends CarComponent with EngineComponent with
FuelTankComponent with GearboxComponent {
override protected val engine = new DieselEngine()
override protected val fuelTank = new FuelTank(capacity = 60)
override protected val gearBox = new FiveGearBox()
override val car = new HondaCar()
}
MyApp.car.drive()
MyApp.car.park()
41. If we want to write a CarTest, we can
provide mock instances for the
components we're not testing:
42. val testCar = new CarComponent with EngineComponent with FuelTankComponent
with GearboxComponent {
override protected val engine = mock[Engine]
// mock engine
override protected val fuelTank = mock[FuelTank]
// mock tank
override protected val gearBox = new FiveGearBox() // an actual gearbox
override val car = new HondaCar()
}.car
44. The Cake Pattern
+ Environment specific assembly of components
+ Compile-time checked, typesafe
+ Everything is immutable
+ No external DI descriptor
- Can become hard to read and understand
- May be difficult to configure components
- No control over initialization order
- Self-types prone to fragile base class problem
45. Remember this:
trait Car {
engine: Engine =>
def drive() { /* start engine */ }
def park() { /* stop engine */ }
}
object MyCar extends Car with DieselEngine
How did they do it?
Let's decompile MyCar.class
46. > javap com.example.MyCar
Compiled from "Car.scala"
public final class com.example.MyCar extends java.lang.Object{
public static void park();
public static void drive();
public static boolean isRunning();
public static void stop();
public static void start();
/* ⊠*/
}
All functions from Car and Engine have been
"lifted" into the top-level class!
47. Imagine Car and Engine are part of a library that
I'm using to construct MyCar.
Any change to library-private interactions
between these traits are not reflected in MyCar.
class