Класс Case для Json: шаблон проектирования о том, как избежать повторяющегося сопоставления шаблонов без использования внешней библиотеки Json

Я изучаю кейс-классы Scala и шаблоны проектирования. С этой целью я создал приведенный ниже пример, который, как мне кажется, является довольно вероятным сценарием при работе с данными типа Json. Я знаю, что существуют библиотеки, которые делают это, но я делаю это вручную, чтобы изучить подходы Scala к решению проблем, так как использование библиотеки не поможет мне учиться.

Основное улучшение дизайна, которое я хочу сделать, — это абстрагирование общего кода.

Предположим, моя кодовая база состоит из множества классов case, где каждый класс case сериализуем:

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 при сериализации я в основном просматриваю каждую пару (ключ, значение) (кроме group_name) в списке параметров и делаю следующее:

    ключ: значение.serialize()

Сейчас форму значения не знаю, может быть вариант как в примере. Кроме того, предположим, что у меня есть много классов, таких как Pets. В этом случае мне пришлось бы вручную писать много совпадений с шаблоном для каждого аргумента, где это необходимо, различая String, Int, Option [String] и т. д. Был бы способ абстрагироваться от этой сериализуемой операции, чтобы, если у меня есть много классов case как и с домашними животными, я могу просто запустить одну функцию и получить правильный результат.

Я задал связанный вопрос здесь о получении объявленного поля из классов случаев, но кажется, что этот способ не является безопасным для типов и может создать проблемы позже, если я добавлю больше пользовательских классов случаев:

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


person finite_diffidence    schedule 01.07.2020    source источник
comment
Это не лучшее место для начала изучения Scala, потому что вы помещаете имена символов исходного кода в значения данных. Это связано с использованием reflection, что неудобно в Scala (да и в любом другом языке). Хотя это используется при обработке JSON, как вы говорите, существуют существующие библиотеки, которые выполняют эту работу.   -  person Tim    schedule 01.07.2020
comment
То, что вы хотите здесь сделать, включает отражение во время выполнения или во время компиляции. Хотя ни один из них сам по себе не является сверхсложным, изучение его без твердого понимания остальной части языка является мазохизмом.   -  person Mateusz Kubuszok    schedule 01.07.2020
comment
Спасибо, я отложу свое изучение времени выполнения / отражения времени компиляции. С этим понятием я еще не сталкивался.   -  person finite_diffidence    schedule 01.07.2020
comment
TL;DR; ответом на такую ​​проблему является typelcass.   -  person Luis Miguel Mejía Suárez    schedule 01.07.2020
comment
См. раздел Какую библиотеку JSON использовать в Scala?. Я знаю, что в вашем вопросе говорится, что вы не хотите его использовать, но я все равно рекомендую его.   -  person user    schedule 03.07.2020


Ответы (2)


Это сложная вещь, чтобы сделать вообще. Этот код не всегда использует все поля в выводе (например, group_name), и имя поля не всегда совпадает с именем в строке (например, cat против 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 — это хороший способ форматирования списков и исключения , , в выводе, если один из параметров пуст.

person Tim    schedule 01.07.2020
comment
Круто, мне нравится сопоставление опций. Буду ли я прав, если скажу, что это использует свойство option monad? - person finite_diffidence; 01.07.2020
comment
Я всегда нервничаю, когда появляется слово монада. Я вижу, что это пример согласованности в библиотеке Scala в том, как она обрабатывает подобные коллекции объекты, такие как Option, Try и Future. Такие методы, как map, flatMap и foreach, работают с этими типами ожидаемым образом, как и для обычных коллекций, таких как List. Но я считаю, что map (точнее flatMap) — это один из характерных методов монады. - person Tim; 02.07.2020

Вот общий способ сделать ограниченный метод сериализации для классов case, используя тот факт, что они 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 без имени группы, вы можете определить один val в Pets, который вручную сериализует все поля, кроме 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}}

В Scastie: https://scastie.scala-lang.org/A5slCY65QIKJ2YTNBUPvQA

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

Имейте в виду, что это не будет работать ни для чего, кроме классов case — вам придется использовать отражение.

person user    schedule 01.07.2020