在 Scala 中创建需要不同构造函数参数的子类型的通用方法
Generic way to create subtypes that require different constructor arguments in Scala
在我的单元测试中,我需要生成各种事件,这些事件都继承自抽象 Event
class,但创建方式不同。例如,事件 A 和 B 具有以下签名:
def makeEventA(a: Int, b: String): EventA
def makeEventB(p: String, q: Long, r: Long): EventB
这两个事件的核心逻辑是相同的,并且是在 Event
的任何子 class 上定义的,所以我想创建行为函数并重新使用它们。为此,我正在考虑创建一个 'Maker' 特征,我可以从中创建每个单独的事件并在单元测试中使用结果:
trait EventMaker {
def make[T <: Event](...): T
}
class EventAMaker extends EventMaker {
override def make[EventA](...) = /* custom implementation */
}
// similar for EventBMaker
我的想法是进行单元测试,其中我混合了正确的 class 并在此基础上调用了适当的 make
方法。我想使用 ScalaTest 对 ScalaCheck 的内置支持来改变参数,这样我就不必对所有值和不同的事件进行硬编码,也不必基本上复制粘贴相同的代码(参数列表除外)。
我正在纠结的是 make
的签名。它应该为特定的 class 采用适当的参数,它们在数量、类型和名称上都不同。我想要达到的目标是什至 doable/sensible,有没有更好的方法,或者我该如何进行?
我考虑的替代方案是匹配各种事件并调用相应的 make
方法(工厂设计模式),但这会导致相同的问题:
def make[T <: Event](eventType: T, ...): T = eventType match {
case EventA => new EventMakerA(...)
case EventB => new EventMakerB(...)
case _ => ...
}
另一种选择是对所有参数使用 Option
,默认值为 None
,以便签名匹配。但是,这似乎很浪费,而且并不能完全提高易读性。
我相信,最好的办法是将参数打包到不透明的 Tuple
中,然后动态或静态检查元组是否与 Event
.
所需的参数匹配
假设以下设置:
import scala.reflect._
sealed trait Event
case class EventA(a: Int, b: String) extends Event
object EventA {
// this val is needed only for the dynamic checking approach
final val tag = classTag[EventA]
}
case class EventB(p: String, q: Long, r: Long) extends Event
object EventB {
// this val is needed only for the dynamic checking approach
final val tag = classTag[EventB]
}
动态检查时,您只需匹配参数,并检查它们是否位于具有正确长度和正确元素类型的元组中。这不是类型安全的,并且可以在运行时抛出 ClassCastException
s,如果事件参数具有带有类型参数的类型(例如,l: List[Int]
或 t: (Int, Double)
),因此类型擦除开始。
代码可以用不同的方式组织:
def make[T <: Event, Args](obj: Class[T], args: Args)(implicit tag: ClassTag[T]): T = ((tag, args) match {
case (EventA.tag, (a: Int, b: String)) => EventA(a, b)
case (EventB.tag, (p: String, q: Long, r: Long)) => EventB(p, q, r)
case (otherTag, otherArgs) => sys.error("wrong args for tag")
}).asInstanceOf[T]
scala> make(classOf[EventA], (10, "foo"))
res4: EventA = EventA(10,foo)
scala> make(classOf[EventB], ("bar", 10L, 20L))
res5: EventB = EventB(bar,10,20)
或者您可以将其提取到 class 中,以便能够将确切的 Event
类型作为类型参数传递:
class EventMaker[T <: Event] {
def apply[Args](args: Args)(implicit tag: ClassTag[T]): T = ((tag, args) match {
case (EventA.tag, (a: Int, b: String)) => EventA(a, b)
case (EventB.tag, (p: String, q: Long, r: Long)) => EventB(p, q, r)
case (otherTag, otherArgs) => sys.error("wrong args for tag")
}).asInstanceOf[T]
}
def make2[T <: Event] = new EventMaker[T]
scala> make2[EventA](10, "foo")
res6: EventA = EventA(10,foo)
scala> make2[EventB]("bar", 10L, 20L)
res7: EventB = EventB(bar,10,20)
您还可以使用 shapeless
库(或手写宏)静态检查参数。如果提供的参数与案例 class 所需的参数不匹配,这将导致编译错误。此外,如果单个事件由案例 classes 表示,则此方法效果最佳,但也可以调整以支持具有 shapeless.ops.function.FnToProduct
的某些功能
import shapeless._
class EventMaker3[T <: Event] {
def apply[Args, H <: HList, H0 <: HList](args: Args)(implicit
genObj: Generic.Aux[T, H], // conversion between HList and case class
genArgs: Generic.Aux[Args, H0], // conversion between HList and arguments tuple
ev: H0 =:= H // assert that HList representations of the case class
// and provided arguments are the same
): T = genObj.from(genArgs.to(args))
}
def make3[T <: Event] = new EventMaker3[T]
scala> make3[EventA](10, "foo")
res8: EventA = EventA(10,foo)
scala> make3[EventB]("bar", 10L, 20L)
res9: EventB = EventB(bar,10,20)
scala> make3[EventA](1, 2)
<console>:21: error: Cannot prove that H0 =:= H.
make3[EventA](1, 2)
^
在我的单元测试中,我需要生成各种事件,这些事件都继承自抽象 Event
class,但创建方式不同。例如,事件 A 和 B 具有以下签名:
def makeEventA(a: Int, b: String): EventA
def makeEventB(p: String, q: Long, r: Long): EventB
这两个事件的核心逻辑是相同的,并且是在 Event
的任何子 class 上定义的,所以我想创建行为函数并重新使用它们。为此,我正在考虑创建一个 'Maker' 特征,我可以从中创建每个单独的事件并在单元测试中使用结果:
trait EventMaker {
def make[T <: Event](...): T
}
class EventAMaker extends EventMaker {
override def make[EventA](...) = /* custom implementation */
}
// similar for EventBMaker
我的想法是进行单元测试,其中我混合了正确的 class 并在此基础上调用了适当的 make
方法。我想使用 ScalaTest 对 ScalaCheck 的内置支持来改变参数,这样我就不必对所有值和不同的事件进行硬编码,也不必基本上复制粘贴相同的代码(参数列表除外)。
我正在纠结的是 make
的签名。它应该为特定的 class 采用适当的参数,它们在数量、类型和名称上都不同。我想要达到的目标是什至 doable/sensible,有没有更好的方法,或者我该如何进行?
我考虑的替代方案是匹配各种事件并调用相应的 make
方法(工厂设计模式),但这会导致相同的问题:
def make[T <: Event](eventType: T, ...): T = eventType match {
case EventA => new EventMakerA(...)
case EventB => new EventMakerB(...)
case _ => ...
}
另一种选择是对所有参数使用 Option
,默认值为 None
,以便签名匹配。但是,这似乎很浪费,而且并不能完全提高易读性。
我相信,最好的办法是将参数打包到不透明的 Tuple
中,然后动态或静态检查元组是否与 Event
.
假设以下设置:
import scala.reflect._
sealed trait Event
case class EventA(a: Int, b: String) extends Event
object EventA {
// this val is needed only for the dynamic checking approach
final val tag = classTag[EventA]
}
case class EventB(p: String, q: Long, r: Long) extends Event
object EventB {
// this val is needed only for the dynamic checking approach
final val tag = classTag[EventB]
}
动态检查时,您只需匹配参数,并检查它们是否位于具有正确长度和正确元素类型的元组中。这不是类型安全的,并且可以在运行时抛出 ClassCastException
s,如果事件参数具有带有类型参数的类型(例如,l: List[Int]
或 t: (Int, Double)
),因此类型擦除开始。
代码可以用不同的方式组织:
def make[T <: Event, Args](obj: Class[T], args: Args)(implicit tag: ClassTag[T]): T = ((tag, args) match {
case (EventA.tag, (a: Int, b: String)) => EventA(a, b)
case (EventB.tag, (p: String, q: Long, r: Long)) => EventB(p, q, r)
case (otherTag, otherArgs) => sys.error("wrong args for tag")
}).asInstanceOf[T]
scala> make(classOf[EventA], (10, "foo"))
res4: EventA = EventA(10,foo)
scala> make(classOf[EventB], ("bar", 10L, 20L))
res5: EventB = EventB(bar,10,20)
或者您可以将其提取到 class 中,以便能够将确切的 Event
类型作为类型参数传递:
class EventMaker[T <: Event] {
def apply[Args](args: Args)(implicit tag: ClassTag[T]): T = ((tag, args) match {
case (EventA.tag, (a: Int, b: String)) => EventA(a, b)
case (EventB.tag, (p: String, q: Long, r: Long)) => EventB(p, q, r)
case (otherTag, otherArgs) => sys.error("wrong args for tag")
}).asInstanceOf[T]
}
def make2[T <: Event] = new EventMaker[T]
scala> make2[EventA](10, "foo")
res6: EventA = EventA(10,foo)
scala> make2[EventB]("bar", 10L, 20L)
res7: EventB = EventB(bar,10,20)
您还可以使用 shapeless
库(或手写宏)静态检查参数。如果提供的参数与案例 class 所需的参数不匹配,这将导致编译错误。此外,如果单个事件由案例 classes 表示,则此方法效果最佳,但也可以调整以支持具有 shapeless.ops.function.FnToProduct
import shapeless._
class EventMaker3[T <: Event] {
def apply[Args, H <: HList, H0 <: HList](args: Args)(implicit
genObj: Generic.Aux[T, H], // conversion between HList and case class
genArgs: Generic.Aux[Args, H0], // conversion between HList and arguments tuple
ev: H0 =:= H // assert that HList representations of the case class
// and provided arguments are the same
): T = genObj.from(genArgs.to(args))
}
def make3[T <: Event] = new EventMaker3[T]
scala> make3[EventA](10, "foo")
res8: EventA = EventA(10,foo)
scala> make3[EventB]("bar", 10L, 20L)
res9: EventB = EventB(bar,10,20)
scala> make3[EventA](1, 2)
<console>:21: error: Cannot prove that H0 =:= H.
make3[EventA](1, 2)
^