【问题标题】:Scala Builder pattern with phantom types具有幻象类型的 Scala Builder 模式
【发布时间】:2016-02-23 14:09:11
【问题描述】:

在 Scala 中具有以下构建器模式。为了简化它,我使用了A 的3 个实例,这样instance1 只包含field1,并且与field2field3 没有连接。问题是在代码中的任何地方我都必须使用val s = A.instance1.field1.get; doSomething(s),而get 调用可能并不安全。例如A.instance1.field2.get 将在None.get 上失败。为了保护它,我必须将 case 与选项匹配并处理 None 情况:

object A {
  val instance1 = new ABuilder().withField1("abc").build1
  val instance2 = new ABuilder().withField1("abc").withField2("def").build2
  val instance3 = new ABuilder().withField1("abc").withField3("def").build1
}

case class A(builder: ABuilder) {
  val field1: Option[String] = builder.field1
  val field2: Option[String] = builder.field2
  val field3: Option[String] = builder.field3
}

class ABuilder {
  var field1: Option[String] = None
  var field2: Option[String] = None
  var field3: Option[String] = None
  def withField1(f: String): ABuilder = {
    this.field1 = Some(f)
    this
  }
  def withField2(f: String): ABuilder = {
    this.field2 = Some(f)
    this
  }
  def withField3(f: String): ABuilder = {
    this.field3 = Some(f)
    this
  }
  def build1: A = {
    require(field1.isDefined, "field 1 must not be None")
    A(this)
  }
  def build2: A = {
    require(field1.isDefined, "field 1 must not be None")
    require(field2.isDefined, "field 2 must not be None")
    A(this)
  }
}

另一种解决方案是使用参数化类型,也称为幻像类型。我发现很少有关于该主题的好教程,并且在其中找不到如何在 Scala 中使用幻像类型和实际数据(或状态)实现类型安全的构建器模式 - 所有示例仅描述方法。

如何在我的示例中使用幻像类型来避免出现运行时 None 异常并仅获得不错的类型不匹配异常?我正在尝试参数化所有提到的类和方法并使用密封特征,但到目前为止没有成功。

【问题讨论】:

  • field2或field3可以有默认值吗?
  • 不,我试图不使用.getOrElse,而是使用两种不同的构建器子类型。这个例子是关于幻像类型,或参数化类型及其边界。
  • 您不需要幻像类型。只需让您的构建器在构造函数中接受所需的参数:new ABuilder("foo").withField2("bar")
  • 另外,如果您希望field1 始终存在,并且不希望在任何地方使用额外的.get 污染您的代码(可以理解),只需将其声明为String 而不是比Option[String]。而且,将您的 A 声明为这样的案例类似乎令人困惑。要么让它成为一个普通的类,要么把建筑移到一个伴生对象中。
  • @HordonFreeman 不管是字符串还是非字符串,相同的逻辑适用于任何类型。您没有必须使用选项。事实上,您不应该对必填字段使用选项。这很麻烦、令人困惑,并且违背了Option 类型的预期目的。

标签: scala types scala-2.10 builder-pattern


【解决方案1】:

如果你真的想使用幻像类型,你可以这样做

object PhantomExample {
  sealed trait BaseA
  class BaseAWith1 extends BaseA
  final class BaseAWith12 extends BaseAWith1

  object A {
    val instance1 = new ABuilder().withField1("abc").build1
    val instance2 = new ABuilder().withField1("abc").withField2("def").build2
  }

  case class A[AType <: BaseA](builder: ABuilder) {
    def field1[T >: AType <: BaseAWith1] = builder.field1.get
    def field2[T >: AType <: BaseAWith12] = builder.field2.get
  }

  class ABuilder {
    var field1: Option[String] = None
    var field2: Option[String] = None
    def withField1(f: String): ABuilder = {
      this.field1 = Some(f)
      this
    }
    def withField2(f: String): ABuilder = {
      this.field2 = Some(f)
      this
    }
    def build1: A[BaseAWith1] = {
      require(field1.isDefined, "field 1 must not be None")
      A(this)
    }
    def build2: A[BaseAWith12] = {
      require(field1.isDefined, "field 1 must not be None")
      require(field2.isDefined, "field 2 must not be None")
      A(this)
    }
  }

  val x = A.instance1.field1                      //> x  : String = abc
  val x2 = A.instance2.field1                     //> x2  : String = abc
  val x3 = A.instance2.field2                     //> x3  : String = def

  // This gives compilation error
  //val x2 = A.instance1.field2
}

但是,我不建议在生产中使用这种代码。我认为它看起来很难看,编译错误似乎很神秘,恕我直言,这不是最好的解决方案。想想看,如果你的实例如此不同,也许它们甚至不是同一个具体类的实例?

trait BaseA {
  def field1
}
class A1 extends BaseA { }
class A2 extends BaseA { ... def field2 = ... }

【讨论】:

  • 嘿伙计,我真的很想接受你的回答,但我遇到了这个错误:Lower bound doesn't conform to upper bounddef field1[T &gt;: AType &lt;: BaseAWith1] 签名处(def field2 相同)。你能帮忙解决这个问题吗?它是为你编译的吗?
  • 我的代码和你的一模一样,我复制粘贴了sbt 0.13.8scala 2.10.5。不知道如何概括。
  • github.com/lampepfl/dotty/issues/780 是否意味着它不会为scala 2.10 而编译为scala 2.11
  • 我现在不在电脑旁,但我想我是用 scala 2.11 编译的。
  • Scala 2.11 中尝试过,同样的错误 - 使用 IntelliJ14
【解决方案2】:

我不确定这是否是你想要的,但我认为你可以以此为基础。

首先是A类:

case class A(field1: String = "", 
              field2: String = "",
              field3: String = "")

case 类具有空字符串的默认值。这允许我们创建任何 A 对象,并分配任何字段值,而无需关心 None 值。

例如:

val b2 = A("abc", "def")
> b2: A = A(abc,def,)

val b1 = A("abc")
> b1: A = A(abc,,)

val notValidB = A(field2 = "xyz")
> notValidB: A = A(,xyz,)

如您所见,b2 和 b1 是 有效 对象,而 notValidB 无效,因为您的对象需要 field1。

您可以创建另一个函数,该函数使用模式匹配来验证您的 A 对象,然后继续执行确定的操作。

def determineAObj(obj: A): Unit = obj match {
  case A(f1, f2, _) if !f1.isEmpty && !f2.isEmpty => println("Is build2")
  case A(f1, _, _) if !f1.isEmpty => println("Is build1")
  case _ => println("This object doesn't match (build1 | build2)")
}

然后运行:

determineAObj(b1)
> "Is build1"

determineAObj(b2)
> "Is build2"

determineAObj(notValidB)
> "This object doesn't match (build1 | build2)"

【讨论】:

    猜你喜欢
    • 2020-08-23
    • 1970-01-01
    • 2016-04-02
    • 1970-01-01
    • 1970-01-01
    • 2016-09-04
    • 2015-07-09
    • 2020-03-27
    • 2019-03-29
    相关资源
    最近更新 更多