To my best knowledge, the traditional cake pattern usually involves 1 layer of trait nesting, to group operations together. Then, the outer layer declares the actual "service" (here: Add, Mul, Operations) without defining it.
trait AddComponent[T] { this: FooComponent[T] =>
def addition: Add
trait Add {
def constant: T
def plus( t1: T, t2: T ): T
def add( t: T ) = plus( t, constant )
}
}
trait MulComponent[T] { this: BarComponent[T] =>
def multiplication: Mul
trait Mul {
def constant: T
def times( t1: T, t2: T ): T
def mul( t: T ) = times( t, constant )
}
}
trait OperationsComponent[T] { this: Add[T] with Mul[T] =>
def operations: Operations
trait Operations {
def neg( t: T ): T
}
}
Then, when mixing the "...Component" traits together, the dependencies are wired:
trait IntOperations extends Operation[Int] {
class IntAdd extends Add { ... }
class IntMul extends Mul { ... }
}
class MyFooBar extends FooComponent[Int] with BarComponent[Int] with IntOperations {
lazy val addition = new IntAdd
lazy val multiplication = new IntMul
lazy val foo = ...
lazy val bar = ...
}
This solves your particular namespacing problem but name clashes (of "service" definitions) remain a problem of the traditional cake pattern. There is a blog post by Daniel Spiewak demonstrating how that can be solved in general but the solution comes with its own set of (huge) tradeoffs (see this talk).
Hope that helped a bit.
P.S. instead of type parameters it might be better to use abstract types here
T
in each trait (since it should be defined at the "component" level). Am I wrong ? – Kareykari