【问题标题】:Case Class to Json: Design Pattern on How to Avoid Repetitive Pattern Match without Using External Json Lib案例类到 Json:关于如何在不使用外部 Json Lib 的情况下避免重复模式匹配的设计模式
【发布时间】:2020-10-21 20:51:01
【问题描述】:

我正在学习 Scala 案例类和设计模式。为此,我在下面创建了一个示例,我认为这是使用 Json 类型数据时相当可能的场景。我知道存在执行此操作的库,但我正在手动学习 Scala 解决问题的方法,因为使用库不会帮助我学习。

我想做的主要设计改进是抽象通用代码。

假设我的代码库由许多案例类组成,其中每个案例类都是可序列化的:

trait Base {
    def serialize(): String
  }

  trait Animal extends Base
  trait Mammal extends Animal
  trait Reptile extends Animal

  case class Lizard(name: String, tail: Boolean) extends Reptile {
    def serialize(): String = s"""{name: $name, tail: $tail}"""
  }

  case class Cat(name: String, age: Int) extends Mammal {
    def serialize(): String = s"""{name: $name, age: $age}"""
  }

  case class Fish(species: String) extends Animal {
    def serialize(): String = s"""{species: $species}"""
  }

  case class Pets(group_name: String, cat: Option[Cat] = None, lizard: Option[Lizard] = None, fish: Fish) extends Base {
    def serialize(): String = {

      // cat and lizard serialize in a similar fashion
      val cat_object = cat match {
        case Some(c) => s"""cats: ${c.serialize()}"""
        case _ => ""
      }

      val lizard_object = lizard match {
        case Some(d) => s"""lizards: ${d.serialize()}"""
        case _ => ""
      }

      // fish serializes in a different way as it is not an option
      val fish_object = s"""fish: ${fish.serialize()}"""

      s"""{$lizard_object, $cat_object, $fish_object}"""
    }
  }

  val bob = Cat("Bob", 42)
  val jill = Lizard("Jill", true)

  val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish")).serialize()

  println(pets)
}

现在这里有一个重复的模式:

  1. 在 Pets 中,当我进行序列化时,我基本上会遍历参数列表中的每个 (key, value) 对(group_name 除外)并执行以下操作:

    键:value.serialize()

现在我不知道值的形式,它可以是示例中的选项。此外,假设我有很多像宠物这样的课程。在那种情况下,我必须在需要的每个参数上手动编写许多模式匹配,区分 String、Int、Option[String] 等。有没有办法抽象出这个可序列化的操作,这样如果我有很多案例类像 Pets 一样,我可以简单地运行一个函数并获得正确的结果。

我在这里问了一个有关从案例类中获取声明字段的相关问题,但似乎这种方式不安全,如果我添加更多自定义案例类,以后可能会产生问题:

https://stackoverflow.com/questions/62662417/how-to-get-case-class-parameter-key-value-pairs

【问题讨论】:

  • 这不是学习 Scala 的好起点,因为您将源代码符号名称放入数据值中。这涉及使用 reflection ,这在 Scala(或实际上任何语言)中很尴尬。虽然在处理 JSON 时会使用它,但正如您所说,现有的库可以完成这项工作。
  • 你想要在这里做的涉及运行时或编译时反射。虽然这些本身都不是超级难,但在没有扎实掌握其他语言的情况下学习它是一种受虐狂。
  • 谢谢 我将推迟我对运行时/编译时反射的学习。我以前没有遇到过这个概念。
  • TL;DR;这类问题的答案是typelcass。
  • 见What JSON library to use in Scala?。我知道您的问题说您不想使用它,但我仍然推荐它

标签: json scala pattern-matching abstraction case-class


【解决方案1】:

这是一种为案例类创建有限序列化方法的通用方法,利用它们是 Products 的事实。

def basicJson(a: Any): String = a match {
  case Some(x) => basicJson(x)
  case None    => ""
  case p: Product =>
    (p.productElementNames zip p.productIterator.map(basicJson _))
      .map(t => s"${t._1}: ${t._2}")
      .mkString("{", ", ", "}")
  case _       => a.toString
}

如果你想在没有组名的情况下序列化Pets,你可以在Pets 中定义一个val,手动序列化除group_name 之外的所有字段:

val toJson = s"{cat: ${basicJson(cat)}, lizard: ${basicJson(lizard)}, fish: ${basicJson(fish)}}"

这段代码的输出

val bob = Cat("Bob", 42)
val jill = Lizard("Jill", true)

val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish"))

println(pets.toJson)

这是:

{cat: {name: Bob, age: 42}, lizard: {name: Jill, tail: true}, fish: {species: goldfish}}

在斯卡斯蒂:https://scastie.scala-lang.org/A5slCY65QIKJ2YTNBUPvQA

<script src="https://scastie.scala-lang.org/A5slCY65QIKJ2YTNBUPvQA.js"></script>

请记住,这不适用于案例类以外的任何内容 - 您必须使用反射。

【讨论】:

    【解决方案2】:

    这通常是一件棘手的事情。此代码并不总是使用输出中的所有字段(例如group_name),并且字段的名称并不总是与字符串中的名称匹配(例如cat vs cats)

    不过,有一些 Scala 技巧可以使现有代码更简洁:

    trait Base {
      def serial: String
    }
    
    trait Animal extends Base
    trait Mammal extends Animal
    trait Reptile extends Animal
    
    case class Lizard(name: String, tail: Boolean) extends Reptile {
      val serial: String = s"""{name: $name, tail: $tail}"""
    }
    
    case class Cat(name: String, age: Int) extends Mammal {
      val serial: String = s"""{name: $name, age: $age}"""
    }
    
    case class Fish(species: String) extends Animal {
      val serial: String = s"""{species: $species}"""
    }
    
    case class Pets(group_name: String, cat: Option[Cat] = None, lizard: Option[Lizard] = None, fish: Fish) extends Base {
      val serial: String = {
        // cat and lizard serialize in a similar fashion
        val cat_object = cat.map("cats: " + _.serial)
    
        val lizard_object = lizard.map("lizards: " + _.serial)
    
        // fish serializes in a different way as it is not an option
        val fish_object = Some(s"""fish: ${fish.serial}""")
    
        List(lizard_object, cat_object, fish_object).flatten.mkString("{", ", ", "}")
      }
    }
    
    val bob = Cat("Bob", 42)
    val jill = Lizard("Jill", true)
    
    val pets = Pets("my group", Some(bob), Some(jill), Fish("goldfish")).serial
    
    println(pets)
    

    由于case class 是不可变的,因此序列化的值不会改变,因此让它看起来像一个名为serial 的属性更有意义。

    Option 值最好在 Option 内部使用 map 处理,然后在最后提取。在这种情况下,我使用flatten 将List[Option[String]] 转换为List[String]。

    mkString 方法是格式化列表的好方法,如果其中一个选项为空,则可以避免在输出中出现 , ,。

    【讨论】:

    • 酷,我喜欢选项的映射。我说这是在使用 option monad 属性是否正确?
    • 当“monad”这个词出现时,我总是很紧张。我认为这是 Scala 库在处理 Option、Try 和 Future 等类似集合的对象方面的一致性示例。诸如map、flatMap 和foreach 之类的方法在这些类型上以“预期的方式”运行,就像对List 之类的普通集合一样。但我相信map(或者更确切地说是flatMap)是monad的特征方法之一。
    猜你喜欢
    • 1970-01-01
    • 2011-10-14
    • 1970-01-01
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-04
    • 1970-01-01
    相关资源
    最近更新 更多