My question is very similar to Scala Cake Pattern and Dependency Collisions. But I'm struggling to find a concrete solution as suggested in Daniel C's answer.
So here is the problem:
A ProductDetailsPage
(trait) requires two independent service modules ProductServiceModule
and SessionModule
, implemented by ProductServiceModuleWs
and SessionModuleWs
respectively.
Both modules rely on a RestServiceConfigurationProvider
.
For this RestServiceConfigurationProvider
, there is only one concrete implementation available: DefaultRestServiceConfigurationProvider
(atm).
The DefaultRestServiceConfigurationProvider
on the other hand depends on theRestEndpointProvider
which can either be a HybrisEndpointProvider
or a ProductServiceEndpointProvider
In short, ProductServiceModuleWs
and SessionModuleWs
connect to remote RESTful web services. The exact IP address of the particular service is provided by an implementation of the RestEndpointProvider.
Now, this is where the collisions happens. Feel free to try out the code below. The troublesome place where the dependency collision happens is marked by a comment.
Rightfully so, the compiler complains about the two conflicting implementations of the RestEndpointProvider
, namely HybrisEndpointProvider
and ProductServiceEndpointProvider
As Daniel mentioned in his answer, to avoid any such collisions, I should wire up the ProductServiceModuleWs
and SessionModuleWs
separately, each with its own concrete RestEndpointProvider implementation, perhaps like so
new ProductServiceModuleWs
with DefaultRestServiceConfigurationProvider
with ProductServiceEndpointProvider
new SessionModuleWs
with DefaultRestServiceConfigurationProvider
with HybrisEndpointProvider
But here is where I got stuck.
How can those two separately configured modules be now injected into the ProductDetailsPage
avoiding dependency collisions, but still utilizing the cake pattern?
Here is the example code. The code is self contained and should run in your IDE.
case class RestEndpoint(url: String, username: Option[String] = None, password: Option[String] = None)
trait RestEndpointKey {
def configurationKey: String
}
case object HybrisEndpointKey extends RestEndpointKey { val configurationKey = "rest.endpoint.hybris" }
case object ProductServiceEndpointKey extends RestEndpointKey { val configurationKey = "rest.endpoint.productservice" }
trait ProductDetailsPage {
self: ProductServiceModule with SessionModule =>
}
trait ProductServiceModule {}
trait SessionModule {}
trait ProductServiceModuleWs extends ProductServiceModule {
self: RestServiceConfigurationProvider =>
}
trait SessionModuleWs extends SessionModule {
self: RestServiceConfigurationProvider =>
}
trait RestServiceConfigurationProvider {}
trait DefaultRestServiceConfigurationProvider extends RestServiceConfigurationProvider {
self: RestEndpointProvider =>
}
sealed trait RestEndpointProvider {
def endpointKey: RestEndpointKey
}
trait HybrisEndpointProvider extends RestEndpointProvider {
val endpointKey = HybrisEndpointKey
}
trait ProductServiceEndpointProvider extends RestEndpointProvider {
val endpointKey = ProductServiceEndpointKey
}
object Example extends App {
new ProductDetailsPage
with ProductServiceModuleWs
with SessionModuleWs
with DefaultRestServiceConfigurationProvider
with HybrisEndpointProvider
with ProductServiceEndpointProvider /// collision, since HybrisEndpointProvider already defined the endpointKey !!!!!
}
}
Implicit scope gives you some control over where you pick up values.
Somewhere, you're going to select between a and b by name, whether the name is a term or a type.
If you distinguish them by type, then you can ask for them by type.
The convenience is that you can install a config for Config[Value1]
that would otherwise be a mix-in with a custom member as in your example.
As shown, you can also introduce implicits in lexical scope.
package conflict
case class Value(s: String)
trait Value1 extends Value
object Value1 {
implicit val v: Config[Value1] = new Config[Value1] { def value = new Value("hi") with Value1 }
}
trait Value2 extends Value
object Value2 {
implicit val v: Config[Value2] = new Config[Value2] { def value = new Value("bye") with Value2 }
}
trait Config[A <: Value] { def value: A }
trait Configurator {
def config[A <: Value : Config]: Config[A] = implicitly[Config[A]]
}
trait Consumer1 { _: Configurator =>
def f = config[Value1].value
}
trait Consumer2 { _: Configurator =>
def g = config[Value2].value
}
trait Consumer3 { _: Configurator =>
def h[V <: Value : Config] = config[V].value
}
object Test extends App with Configurator with Consumer1 with Consumer2 with Consumer3 {
Console println s"Using $f"
Console println s"Using $g"
locally {
implicit val `my local config` = new Config[Value2] { def value = new Value("hello again") with Value2 }
Console println s"Using ${h[Value2]}"
}
}