如何为 Scala 子类型实现流畅的接口?

How to implement fluent interface for scala subtypes?

我可以通过以下方式使用流畅的界面实现嵌套 类:

class Animal(name: String, props: Map[String, Any]) {
  def properties: Map[String, Any] = Map("name" -> name) ++ props
  def withAge(age: Int): Animal = new Animal(name, props.updated("age", age)) // can't use built-in copy
}

case class Cat(name: String, lives: Int, props: Map[String, Any]) extends Animal(name, props) {
  override def properties: Map[String, Any] = Map("name" -> name, "lives" -> lives) ++ props
  override def withAge(age: Int): Cat = Cat(name, lives, props.updated("age", age))
}

然而我对此还很不满意。有很多重复,即使我使用继承,我也没有重用任何代码。 我试过使用 this.type 作为 return 类型,甚至使用 zio-prelude 子类型化功能,但一直存在的问题是,在某些时候,子类无法被正确识别。

有没有更好的方法来做到这一点而无需重复并利用 Scala 功能?

理想情况下我想要这样的东西

case class Animal(name: String, props: Map[String, Any]) {
  def properties: Map[String, Any] = Map("name" -> name) ++ props
  def withAge(age: Int): this.type = copy(props = props.updated("age", age))
}

final case class Cat(name: String, lives: Int, props: Map[String, Any]) extends Animal(name, props + ("lives" -> lives))

这样就不会发生重复。当然,以下内容没有编译。

val myCat: Cat = Cat("murzic", 9, Map()).withAge(4)
package animalworld

import scala.collection.mutable

class Builder(val properties: mutable.HashMap[String, Any]) {

  def this() = this(mutable.HashMap.empty)

  def withName(name: String): Builder = {
    properties.put("name", name)
    this
  }

  def withAge(age: Int): Builder = {
    properties.put("age", age)
    this
  }

  def setProperty(key: String, value: Any): Builder = {
    properties.put(key, value)
    this
  }

  def build[A <: Animal](implicit buildable: Buildable[A]): A =
    buildable.build(this)
}

package animalworld

trait Buildable[A <: Animal] {
  def build(builder: Builder): A
}
package animalworld

import scala.collection.mutable

class Animal protected (val name: String, val properties: Map[String, Any]) {
  def toBuilder: Builder = new Builder(mutable.HashMap.from(properties))
}

object Animal {
  implicit val animalBuildable: Buildable[Animal] = { builder =>
    new Animal(
      builder.properties("name").asInstanceOf[String],
      Map.from(builder.properties)
    )
  }
}
package animalworld

class Cat protected (
    override val name: String,
    val lives: Int,
    override val properties: Map[String, Any]
) extends Animal(name, properties)

object Cat {
  implicit val catBuildable: Buildable[Cat] = { builder =>
    new Cat(
      builder.properties("name").asInstanceOf[String],
      builder.properties("lives").asInstanceOf[Int],
      Map.from(builder.properties)
    )
  }
}
package animalworld

object Main extends App {
  val animal1 = new Builder().withName("tim-tim").build[Animal]

  val cat1 = animal1.toBuilder.withAge(10).setProperty("lives", 9).build[Cat]

  println(animal1.name)
  // tim-tim
  println(animal1.properties)
  // Map(name -> tim-tim)

  println(cat1.name)
  // tim-tim
  println(cat1.lives)
  // 9
  println(cat1.properties)
  // Map(name -> tim-tim, lives -> 9, age -> 10)
}