使用特殊规则键入安全的 Scala Builder 模式

Type safe Scala Builder pattern with special rules

我正在尝试创建案例 class 的类型安全构建器,其中它的参数可以是以下类型:

  1. 需要
  2. 可选
  3. 必需但互斥 -> 一种。前任。假设我有 3 个参数:(param1)、(param2、param3)。如果我有 param1,我不能设置 param2 或 param3。如果我可以同时设置param2和param3,但是我不能设置param1
  4. 可选但互斥 -> 与上述逻辑相同,但这些是可选参数。这是可选的,我可以设置 param1 或(param2 和 param3)。

我想出了如何获取必需和可选的案例,但无法获取案例 3 和 4。 任何想法如何进行。

这些检查可以在运行时完成,但我希望这些规则集在编译时实现

case class Person(name: String, /*required*/
                  address: String, /*optional*/
                  city: String, /* reqd exclusive*/
                  county: String, /* reqd exclusive*/
                  state: String /* reqd exclusive*/,
                  ssn: String, /* optional exclusive*/
                  insurance: String, /* opt exclusive*/
                  passport: String /* opt exclusive*/)
// where (city) and (county, state) are required but are mutually exclusive
// (ssn) and (insurance, passport) are optional but are mutually exclusive. 
// If I set passport, I've to set insurance

sealed trait PersonInfo
object PersonInfo {
  sealed trait Empty extends PersonInfo
  sealed trait Name extends PersonInfo
  sealed trait Address extends PersonInfo
  type Required = Empty with Name with Address
}

case class PersonBuilder[T <: PersonInfo]
(name: String = "", address: String = "", city: String = "", county: String = "", 
  state: String = "", ssn: String = "", insurance: String = "",passport: String ="") {

  def withName(name: String): PersonBuilder[T with PersonInfo.Name] =
    this.copy(name = name)

  def withTask(address: String): PersonBuilder[T with PersonInfo.Address ] =
    this.copy(address = address)

  def withCity(city: String): PersonBuilder[T] =
    this.copy(city = city)

  def withCountry(county: String): PersonBuilder[T] =
    this.copy(county = county)

  def withState(state: String): PersonBuilder[T] =
    this.copy(state = state)

  def withSsn(ssn: String): PersonBuilder[T] =
    this.copy(ssn = ssn)

  def withInsurance(insurance: String): PersonBuilder[T] =
    this.copy(insurance = insurance)

  def withPassport(passport: String): PersonBuilder[T] =
    this.copy(passport = passport)

  def build(implicit ev: T =:= PersonInfo.Required): Person =
    Person(name, address, city, county, state, ssn, insurance, passport)
}

这是构建

val testPerson = PersonBuilder[PersonInfo.Empty]()
    .withName("foo")
    .withSsn("bar")

如评论中所述,如果创建构建器不是硬性要求,则可行的选择可能是在类型中明确这些要求,使用总和类型作为独占选择,使用 Options 作为可选一个,如下例所示:

sealed abstract class Location extends Product with Serializable {
  def value: String
}

object Location {
  final case class City(value: String) extends Location
  final case class County(value: String) extends Location
  final case class State(value: String) extends Location
}

sealed abstract class Identity extends Product with Serializable {
  def value: String
}

object Identity {
  final case class Ssn(value: String) extends Identity
  final case class Insurance(value: String) extends Identity
  final case class Passport(value: String) extends Identity
}

final case class Person(
    name: String,
    address: Option[String],
    location: Location,
    identity: Option[Identity],
)

Scala 3 进一步引入了 enums,这使得定义更加紧凑和可读:

enum Location(value: String) {
  case City(value: String) extends Location(value)
  case County(value: String) extends Location(value)
  case State(value: String) extends Location(value)
}

enum Identity(value: String) {
  case Ssn(value: String) extends Identity(value)
  case Insurance(value: String) extends Identity(value)
  case Passport(value: String) extends Identity(value)
}

final case class Person(
    name: String,
    address: Option[String],
    location: Location,
    identity: Option[Identity],
)

并将 Options 默认设置为 None,您将获得与 custom-made 构建器非常相似的体验,无需任何额外代码:

final case class Person(
    name: String,
    location: Location,
    address: Option[String] = None,
    identity: Option[Identity] = None,
)

Person("Alice", Location.City("New York"))
  .copy(identity = Some(Identity.Ssn("123456")))

您可以很容易地进一步细化:

final case class Person(
    name: String,
    location: Location,
    address: Option[String] = None,
    identity: Option[Identity] = None
) {

  def withAddress(address: String): Person =
    this.copy(address = Some(address))

  def withIdentity(identity: Identity): Person =
    this.copy(identity = Some(identity))

}

Person("Alice", Location.City("New York")).withIdentity(Identity.Ssn("123456"))

您可以尝试使用此代码 here on Scastie