在运行时以优雅的方式处理不同消息类型的负载

Handling loads of different message-types at runtime in an elegant way

为了能够处理大量不同的请求类型,我创建了一个 .proto 文件,如下所示:

message Message
{
   string typeId = 1;
   bytes message = 2;
}

我添加了 typeId 以便人们知道实际的 protobuf bytes 代表什么。 (自我描述)

现在我的问题是以优雅的方式处理不同的 "concrete types"。 (注意:如果我简单地使用类似 switch-case 的方法,一切正常!)

我想到了这样的解决方案:

1) 具有不同处理程序必须实现的特征,例如:

trait Handler[T]
{
  def handle(req: T): Any
}

object TestHandler extends Handler[Test]
{
  override def handle(req: Test): String =
  {
    s"A success, $req has been handled by TestHandler
  }
}

object OtherHandler extends Handler[Other]
{
  override def handle(req: Other): String =
  {
    s"A success, $req has been handled by OtherHandler
  }
} 

2) 提供某种注册表来查询给定消息的正确处理程序:

val handlers = Map(
    Test -> TestHandler,
    Other -> OtherHandler
  )

3) 如果请求进来,它会标识自己,所以我们需要另一个 Mapper:

val reqMapper = Map(
  "Test" -> Test
  "Other" -> Other
)

4) 如果有请求进来,处理它:

val request ...
// Determine the requestType
val requestType = reqMapper(request.type) 
// Find the correct handler for the requestType
val handler = handlers(requestType)
// Parse the actual request
val actualRequest = requestType.parse(...) // type of actualRequest can only be Test or Other in our little example

现在,到这里为止,一切看起来都还不错,但随后这条线打破了我的整个世界:

handler.handle(actualRequest)

它导致:

type mismatch; found : com.trueaccord.scalapb.GeneratedMessage with Product with com.trueaccord.scalapb.Message[_ >: tld.test.proto.Message.Test with tld.test.proto.Message.Other <: com.trueaccord.scalapb.GeneratedMessage with Product] with com.trueaccord.lenses.Updatable[_ >: tld.test.proto.Message.Other with tld.test.proto.Message.Test <: com.trueaccord.scalapb.GeneratedMessage with Product]{def companion: Serializable} required: _1

据我了解 - 如果有误请在此处更正 - 编译器在这里无法确定 actualRequest 是 "handable"处理程序。这意味着它不知道 actualRequest 肯定在那个 mapper 中的某个地方,并且还不知道它有一个 handler

这基本上是人类可以获得的隐含知识,但编译器无法推断。

所以,话虽这么说,我怎样才能优雅地克服这种情况?

当您使用普通地图时,您的类型会丢失。例如

object Test{}
object Other{}
val reqMapper = Map("Test" -> Test,"Other" -> Other)
reqMapper("Test")
res0: Object = Test$@5bf0fe62 // the type is lost here and is set to java.lang.Object

最常用的方法是使用模式匹配

request match {
  case x: Test => TestHandler(x)
  case x: Other => OtherHandler(x)
  case _ => throw new IllegalArgumentException("not supported")
}

如果您仍想使用映射来存储您的类型与处理程序的关系,请考虑由 Shapeless here

提供的 HMap

Heterogenous maps

Shapeless provides a heterogenous map which supports an arbitrary relation between the key type and the corresponding value type,

您可以使用的一个技巧是将伴随对象捕获为隐式对象,并将解析和处理组合在一个函数中,其中类型可供编译器使用:

case class Handler[T <: GeneratedMessage with Message[T]](handler: T => Unit)(implicit cmp: GeneratedMessageCompanion[T]) {
  def handle(bytes: ByteString): Unit = {
    val msg: T = cmp.parseFrom(bytes.newInputStream)
    handler(t)
  }
}

val handlers: Map[String, Handler[_]] = Map(
  "X" -> Handler((x: X) => Unit),
  "Y" -> Handler((x: Y) => Unit)
)

// To handle the request:
handlers(request.typeId).handle(request.message)

此外,请查看 any.proto,它定义的结构与您的 Message 非常相似。它不会解决您的问题,但您可以利用它的 packunpack 方法。

我暂时选择了这个解决方案(基本上是 thesamet 的,稍微适应了我的特定 use-case)

trait Handler[T <: GeneratedMessage with Message[T], R]
{
    implicit val cmp: GeneratedMessageCompanion[T]
    def handle(bytes: ByteString): R = {
        val msg: T = cmp.parseFrom(bytes.newInput())
        handler(msg)
    }

    def apply(t: T): R
}

object Test extends Handler[Test, String]
{
    override def apply(t: Test): String = s"$t received and handled"

    override implicit val cmp: GeneratedMessageCompanion[Test] = Test.messageCompanion
}