Need a design advice


#1

I have a library that describes interfaces and provides core functionality based on this interfaces. I also have concrete implementations for the interfaces in another library. The interfaces in the core project describes only the functionality that is used by the core implementations. The following code describes the scenario:

//////////////////////////////////////
// Core functionality
//////////////////////////////////////

trait Credentials
case class Authenticator() extends Credentials

/**
 * Allows to modify a framework specific request implementation.
 *
 * @tparam R The type of the request.
 */
trait RequestPipeline[R] {
  val request: R
  def withHeader(): RequestPipeline[R]
  def withCookie(): RequestPipeline[R]
}

/**
 * A provider that can be hooked into a request.
 *
 * It scans the request for credentials and returns it.
 *
 * @tparam C The type of the credential.
 */
trait RequestProvider[C <: Credentials] {
  def authenticate[R](request: RequestPipeline[R]): Future[C]
}

/**
 * A core implementation of the request provider.
 */
class CoreRequestProvider extends RequestProvider[Authenticator] {
  override def authenticate[R](request: RequestPipeline[R]): Future[Authenticator] =
    Future.successful(Authenticator())
}

//////////////////////////////////////
// Concrete implementations for Play
//////////////////////////////////////

case class PlayRequest[B](body: B)
case class PlayRequestPipeline[B](request: PlayRequest[B]) extends 
RequestPipeline[PlayRequest[B]] {
  def withHeader(): PlayRequestPipeline[B] = copy(request)
  def withCookie(): PlayRequestPipeline[B] = copy(request)
}

class PlayRequestProvider extends RequestProvider[Authenticator] {
  override def authenticate[R](request: RequestPipeline[R]): Future[Authenticator] =
    Future.successful(Authenticator())
}

case class PlayEnvironment(requestProviders: List[RequestProvider[_ <: Credentials]])

object PlayEnvironment {
  val environment = PlayEnvironment(List(
    new CoreRequestProvider(),
    new PlayRequestProvider()
  ))

  val request = PlayRequestPipeline(PlayRequest("some body"))
  environment.requestProviders.map(_.authenticate(request))
}

Now, in the concrete implementation for Play I would like that the PlayRequestProvider implementation can use the PlayRequestPipeline, because it can unwrap the PlayRequest which provides far more functionality, than a RequestPipeline implementation. How could I do that in an elegant way?

One possibility would be to use a ClassTag in the RequestProvider.authenticate method. This allows me to pattern match on the request. This isn’t very elegant, because in the Play context I know that I use the PlayRequestProvider.

Does anyone have a good design advice?


#2

Another option would be to add an additional type parameter to the RequestProvider trait and remove the type parameter from the RequestProvider.authenticate method. But than the type of the body must be known at compile time and not at runtime which makes it really uncomfortable.

trait RequestProvider[R, C <: Credentials] {
  def authenticate(request: RequestPipeline[R]): Future[C]
}

class CoreRequestProvider[R] extends RequestProvider[R, Authenticator] {
  override def authenticate(request: RequestPipeline[R]): Future[Authenticator] =
    Future.successful(Authenticator())
}

class PlayRequestProvider[B] extends RequestProvider[PlayRequest[B], Authenticator] {
  override def authenticate(request: RequestPipeline[PlayRequest[B]]): Future[Authenticator] =
    Future.successful(Authenticator())
}

case class PlayEnvironment[B](requestProviders: List[RequestProvider[PlayRequest[B], _ <: Credentials]])

object PlayEnvironment {
  val environment = PlayEnvironment[String](List(
    new CoreRequestProvider(),
    new PlayRequestProvider()
  ))

  val request = PlayRequestPipeline(PlayRequest("some body"))
  environment.requestProviders.map(_.authenticate(request))
}

A good solution would be to use a higher kinded type on the RequestProvider. That would eliminate my previous compile time problem. But the new issue is that it accepts only request types with a type parameter like the PlayRequest. But it can be the case, that another framework don’t use a first-order type.

trait RequestProvider[R[_], C <: Credentials] {
  def authenticate[B](request: RequestPipeline[R[B]]): Future[C]
}

class CoreRequestProvider[R[_]] extends RequestProvider[R, Authenticator] {
  override def authenticate[B](request: RequestPipeline[R[B]]): Future[Authenticator] =
    Future.successful(Authenticator())
}

class PlayRequestProvider extends RequestProvider[PlayRequest, Authenticator] {
  override def authenticate[B](request: RequestPipeline[PlayRequest[B]]): Future[Authenticator] =
    Future.successful(Authenticator())
}

case class PlayEnvironment(requestProviders: List[RequestProvider[PlayRequest, _ <: Credentials]])

object PlayEnvironment {
  val environment = PlayEnvironment(List(
    new CoreRequestProvider(),
    new PlayRequestProvider()
  ))

  val request = PlayRequestPipeline(PlayRequest("some body"))
  environment.requestProviders.map(_.authenticate(request))
}

The last issue could be solved with a type alias for a proper type.

case class FooRequest(body: Array[Byte])
object FooRequest {
  type Kind[_] = FooRequest
}
case class FooRequestPipeline(request: FooRequest) extends
  RequestPipeline[FooRequest] {
  def withHeader(): FooRequestPipeline = copy(request)
  def withCookie(): FooRequestPipeline = copy(request)
}

class FooRequestProvider extends RequestProvider[FooRequest.Kind, Authenticator] {
  override def authenticate[B](request: RequestPipeline[FooRequest]): Future[Authenticator] =
    Future.successful(Authenticator())
}

case class FooEnvironment(requestProviders: List[RequestProvider[FooRequest.Kind, _ <: Credentials]])

object FooEnvironment {
  val environment = FooEnvironment(List(
    new CoreRequestProvider(),
    new FooRequestProvider()
  ))

  val request = FooRequestPipeline(FooRequest("some body".getBytes))
  environment.requestProviders.map(_.authenticate(request))
}

#3

I wonder whether you really need all the type parameters. Do you need a different RequestPipeline or RequestProvider for every different type of payload?
I wonder whether you can’t get further with type members, and perhaps some existential here and there. Especially the type parameter of authenticate seems like it could be modelled as an existential.
E.g.

import scala.concurrent.Future

//////////////////////////////////////
// Core functionality
//////////////////////////////////////

trait Credentials
case class Authenticator() extends Credentials

/**
 * Allows to modify a framework specific request implementation.
 *
 * @tparam R The type of the request.
 */
trait RequestPipeline {
  type R
  val request: R
  def withHeader(): RequestPipeline
  def withCookie(): RequestPipeline
}

/**
 * A provider that can be hooked into a request.
 *
 * It scans the request for credentials and returns it.
 *
 * @tparam C The type of the credential.
 */
trait RequestProvider[P <: RequestPipeline, C <: Credentials] {
  def authenticate(request: P): Future[C]
}

/**
 * A core implementation of the request provider.
 */
class CoreRequestProvider[P <: RequestPipeline] extends RequestProvider[P, Authenticator] {
  override def authenticate(request: P): Future[Authenticator] =
    Future.successful(Authenticator())
}

//////////////////////////////////////
// Concrete implementations for Play
//////////////////////////////////////

case class PlayRequest[B](body: B)
case class PlayRequestPipeline(request: PlayRequest[_]) extends RequestPipeline {
  type R = PlayRequest[_]
  def withHeader(): PlayRequestPipeline = copy(request)
  def withCookie(): PlayRequestPipeline = copy(request)
}

class PlayRequestProvider extends RequestProvider[PlayRequestPipeline, Authenticator] {
  override def authenticate(request: PlayRequestPipeline): Future[Authenticator] =
    Future.successful(Authenticator())
}

case class PlayEnvironment(requestProviders: List[RequestProvider[PlayRequestPipeline, _ <: Credentials]])

object PlayEnvironment {
  val environment = PlayEnvironment(List(
    new CoreRequestProvider(),
    new PlayRequestProvider()
  ))

  val request = PlayRequestPipeline(PlayRequest("some body"))
  environment.requestProviders.map(_.authenticate(request))
}

#4

Hi,

Thanks for your answer! I think the type definition on the RequestPipeline trait is needed, because a request pipeline instance gets passed around through different functions with a signature similar to:

def process[R](requestPipeline: RequestPipeline[R]): RequestPipeline[R]

Without the type definition the type of the enclosed request implementation gets lost and then it cannot be unboxed.

I think I’ve found a good solution for now:
RequestPipeline.scala
SilhouetteRequestPipeline.scala
PlayRequestPipeline.scala
RequestProvider.scala
AuthenticatorProvider

Best regards,
Christian