scalacake-pattern

Scala Cake Pattern: How to avoid Dependency Collisions?


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 !!!!! 
   }
}

Solution

  • 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]}"
      }
    }