使用Scala case class 进行建模

6

我试图将来自REST API的响应建模为可以使用模式匹配的案例类。

我认为这是一个很好的适应方式,假设继承,但我看到这已经被弃用了。 我知道已经有与案例类和继承相关的问题,但我的问题更多地涉及如何在此处“没有”继承的情况下正确地建模以下内容。

我从以下两个案例类开始,它们工作得很好:

case class Body(contentType: String, content: String)
case class Response(statusCode: Int, body: Body)

例如,一个REST调用会返回这样的内容:
Response(200, Body("application/json", """{ "foo": "bar" }"""))

我可以像这样进行模式匹配:

response match {
  case Response(200, Body("application/json", json)) => println(json)
  case Response(200, Body("text/xml", xml)) => println(xml)
  case Response(_,_) => println("Something unexpected")
}

等等。这些都很好用。

我遇到的问题是:我想为这些case class提供辅助扩展,例如:

case class OK(body: Body) extends Response(200, body)
case class NotFound() extends Response(404, Body("text/plain", "Not Found"))

case class JSON(json: String) extends Body("application/json", json)
case class XML(xml: String) extends Body("text/xml", xml)

这样我就可以进行简单的模式匹配,例如:

response match {
  case OK(JSON(json)) => println(json)
  case OK(XML(xml)) => println(xml)
  case NotFound() => println("Something is not there")

  // And still drop down to this if necessary:
  case Response(302, _) => println("It moved")
}

此外,这也能让我的REST代码直接使用并返回:

Response(code, Body(contentType, content))

建立动态响应更容易。

因此...

我可以通过以下方式编译它(带有弃用警告):

case class OK(override val body: Body) extends Response(200, body)

然而,这似乎在模式匹配中无效。
Response(200, Body("application/json", "")) match {
  case OK(_) => ":-)"
  case _ => ":-("
}
res0: java.lang.String = :-(

你有没有任何想法可以使这个工作?我愿意考虑不同的方法,但这是我试图找到实用案例类的尝试。

3个回答

10
有几个原因说明为什么 不应该 将case类作为子类。在您的情况下,问题在于OK是另一种类型而不是(子类型)Response,因此匹配失败(即使参数匹配,类型也不匹配)。
相反,您可以使用 自定义提取器。例如:
case class Response(code: Int, body: String)
object OK {
  def apply(body: String) = Response(200, body)
  def unapply(m: Response): Option[String] = m match {
    case Response(200, body) => Some(body)
    case _                   => None
  }
}

def test(m: Response): String = m match {
   case OK(_) => ":-)"
   case _     => ":-("
}

test(Response(300, "Hallo"))  // :-(
test(Response(200, "Welt"))   // :-)
test(OK("Welt"))              // :-)

这里有一些关于自定义提取器的更多例子,请参考此线程


啊,谢谢 - 我完全没有意识到 unapply 的用途,直到现在;这非常有帮助。我将会用我的代码充分测试它,以确保我已经覆盖了所有情况,并会在今晚之后接受。 - 7zark7
很好的回答@Sciss。自定义提取器是我喜欢Scala的东西之一。 - mergeconflict
请注意,当您使用自定义提取器时,您将失去密封类的全面性保证。 - Daniel C. Sobral
@DanielC.Sobral 是的,我看到了 - 我更喜欢在这里严格使用 case classes,但是根据我上面概述的使用要求,这似乎不可行。 - 7zark7

1

虽然0__提到的自定义提取器确实可以使用,但您将失去密封类型层次结构的详尽性保证。虽然在您在问题中提供的示例中没有任何sealed,但该问题非常适合它们。

在这种情况下,我的建议是确保case class始终位于类型层次结构的底部,并使上层类正常。例如:

sealed class Response(val statusCode: Int, val body: Body) sealed
case class Ok(override val body: Body) extends Response(200, body)
sealed class NotOk(statusCode: Int, body: Body) extends Response(statusCode, body)
case object NotFound extends NotOk(404, "Not found")
// and so on...

谢谢 Daniel,我的第一印象是如果我还想允许在响应上进行匹配,这种方法可能行不通 - 我发现如果我像Sciss所提到的那样在Response对象上定义unapply,并将“helpers”设置为case类,那么这种方法可能可行。今天会尝试这两种方法,看看哪个最适合/最有效。 - 7zark7
你是不是想写成 sealed class Response - 0__
@Sciss 是的,还有 NotOk。谢谢指出我的错误。 - Daniel C. Sobral

1

我看了一下,但是我放弃了,因为有太多幻灯片,每张幻灯片只有一个句子/几个单词。也许有没有一个单页版本可以澄清Unfiltered的全部内容? - KajMagnus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接