【发布时间】:2014-06-12 17:50:39
【问题描述】:
总结:
我希望在实例化 Scala 案例类之前有验证选项,而不是必须使用 requires/IllegalArgumentException 机制。在尝试预先验证用于 Scala 案例类实例化的参数时,有没有办法避免类似 Java 的样板代码?
详情:
来自 Java,我现在已经使用了足够多的 Scala 案例类来彻底享受和欣赏它消除了多少样板。但是,我现在遇到了一个问题,这似乎是我的 Scala 代码在案例类周围大量膨胀的样板。
请为我的典型 Scala 案例类考虑以下代码(直接从 IntelliJ 的 Scala 工作表复制 - 注意:案例类 Surface1 之上的所有内容都是为了在呈现 Surface2 时减少代码噪音):
val LONGITUDE_MAX = 180.0d
val LATITUDE_MAX = 90.0d
def isLongitudeValid(longitude: Double) = (-LONGITUDE_MAX <= longitude) && (longitude <= LONGITUDE_MAX)
def isLatitudeValid(latitude: Double) = (-LATITUDE_MAX <= latitude) && (latitude <= LATITUDE_MAX)
object SurfaceType extends Enumeration {
type SurfaceType = Value
val SEA, LAND, ICE = Value
}
def isSurfaceTypeCorrect(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value) = true //fabricated validation assuming longitude and latitude are validated
//
//#1. Simple case class use
case class Surface1(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) {
require(isLongitudeValid(longitude), s"longitude [$longitude] must be greater than or equal to -$LONGITUDE_MAX and less than or equal to $LONGITUDE_MAX")
require(isLatitudeValid(latitude), s"latitude [$latitude] must be greater than or equal to -$LATITUDE_MAX and less than or equal to $LATITUDE_MAX")
require(isSurfaceTypeCorrect(longitude, latitude, surfaceType), s"for longitude[$longitude] and latitude[$latitude], surfaceType [$surfaceType] must be the correct")
}
因此,鉴于以上 Surface1 的情况,以下是一些示例用法:
val surface1a = Surface1()
val surface1b = Surface1(0.0d, 0.0d, SurfaceType.SEA)
val surface1c = Surface1(-180.0d, -90.0d, SurfaceType.ICE)
val surface1d = Surface1(-180.1d, -90.1d, SurfaceType.ICE) //generates an IllegalArgumentException
前三个将生成正确的实例。最后一个将生成 IllegalArgumentException...但仅适用于经度。纬度也是错误的。但是对于标准的 Scala 案例类模型,不会评估对表面类型的纬度验证。至少 latitude 的验证也已执行并收到异常列表将非常有帮助。
所以,经过无数次切线,这就是我能够在我可以做任何事情的地方进行预实例化验证的结果;继续使用标准的 requires/exception 模型进行实例化(应用),返回一个选项(获取)或返回一个(创建):
//#2. Explicit validators for case class use
type CaseClassValidationException = IllegalArgumentException
object Surface2 {
def validate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[List[CaseClassValidationException]] = {
val errorsA =
List(
if (!isLongitudeValid(longitude)) Some(new CaseClassValidationException(s"longitude [$longitude] must be greater than or equal to -$LONGITUDE_MAX and less than or equal to $LONGITUDE_MAX")) else None
, if (!isLatitudeValid(latitude)) Some(new CaseClassValidationException(s"latitude [$latitude] must be greater than or equal to -$LATITUDE_MAX and less than or equal to $LATITUDE_MAX")) else None
)
val errorsB =
if (errorsA.isEmpty) //these checks depend upon the errorsA checks
List(
if (!isSurfaceTypeCorrect(longitude, latitude, surfaceType))
Some(new CaseClassValidationException(s"for longitude[$longitude] and latitude[$latitude], surfaceType [$surfaceType] must be the correct"))
else None
)
else Nil
val errors = (errorsA ::: errorsB).flatten
if (!errors.isEmpty) Some(errors)
else None
}
def get(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Option[Surface2] = {
create(longitude, latitude, surfaceType) match {
case Right(surface2) => Some(surface2)
case Left(errors) => None
}
}
def create(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Either[List[CaseClassValidationException], Surface2] = {
validate(longitude, latitude, surfaceType) match {
case Some(errors) => Left(errors)
case None => Right(new Surface2(longitude, latitude, surfaceType, false))
}
}
}
case class Surface2 private (longitude: Double, latitude: Double, surfaceType: SurfaceType.Value, executeValidate: Boolean) {
def this(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA) = this(longitude, latitude, surfaceType, true)
if (executeValidate) require(Surface2.validate(longitude, latitude, surfaceType).isEmpty, "failed validate")
}
因此,鉴于以上 Surface2 的情况,以下是一些示例用法:
val surface2a = new Surface2()
val surface2b = new Surface2(0.0d, 0.0d, SurfaceType.SEA)
val surface2c = new Surface2(-180.0d, -90.0d, SurfaceType.ICE)
val surface2d = new Surface2(-180.1d, -90.1d, SurfaceType.ICE) //generates an IllegalArgumentException
val surface2option1a = Surface2.get()
val surface2option1b = Surface2.get(0.0d, 0.0d, SurfaceType.SEA)
val surface2option1c = Surface2.get(-180.0d, -90.0d, SurfaceType.ICE)
val surface2option1d = Surface2.get(-180.1d, -90.1d, SurfaceType.ICE) //None
val surface2either1a = Surface2.create()
val surface2either1b = Surface2.create(0.0d, 0.0d, SurfaceType.SEA)
val surface2either1c = Surface2.create(-180.0d, -90.0d, SurfaceType.ICE)
val surface2either1d = Surface2.create(-180.1d, -90.1d, SurfaceType.ICE) //Left[...]
前四个产生与 Surface1 相同的结果。但是,剩下的 8 个只会在 validate 方法返回 None 时实例化。所以,我已经达到了我想要的效果,但代价是代码气味。以下是我在解决方案中遇到的问题:
- 额外的 executeValidate 参数导致实例化空间使用效率低下
- 公开的实现细节 - executeValidate 参数
- 更大的代码表面(即样板)增加了引入错误/错误的可能性
我有几十个案例类将在我当前的项目中使用。当我定义它时,必须将它添加到每个案例类中确实感觉非常沉重。当然,我忽略了一些东西,我可以在没有列出不想要的效果的情况下大大简化产生所需效果的过程。
如果您能提供任何指导,我们将不胜感激。
2014 年 6 月 13 日更新:
看来(不使用 Scalaz)没有真正的方法可以显着减少样板。也就是说,感谢 NikitaVolkov 和他与我的偏好保持一致,即尽可能避免使用异常,我意识到我不需要案例类本身中的“应用”方法(这就是强制执行效率低下的原因)私有案例类构造函数)。这是对案例类本身的重大简化;即完全删除抛出异常的应用方法。
下面是最新版本,Surface3,它实现了我想要的所有效果,虽然更短更简单,但在样板方面仍然很长:
object SurfaceType extends Enumeration {
type SurfaceType = Value
val SEA, LAND, ICE = Value
}
type CaseClassValidationException = IllegalArgumentException
object Surface3 {
def longitudeValidate(longitude: Double): Option[CaseClassValidationException] = {
val longitudeBound = 180.0d
if (!((-longitudeBound <= longitude) && (longitude <= longitudeBound)))
Some(new CaseClassValidationException(s"longitude [$longitude] must be greater than or equal to -$longitudeBound and less than or equal to $longitudeBound"))
else None
}
def latitudeValidate(latitude: Double): Option[CaseClassValidationException] = {
val latitudeBound = 90.0d
if (!((-latitudeBound <= latitude) && (latitude <= latitudeBound)))
Some(new CaseClassValidationException(s"latitude [$latitude] must be greater than or equal to -$latitudeBound and less than or equal to $latitudeBound"))
else None
}
def surfaceTypeValidate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[CaseClassValidationException] = None //fabricated validation assuming longitude and latitude are validated
private def fullValidate(longitude: Double, latitude: Double, surfaceType: SurfaceType.Value): Option[List[CaseClassValidationException]] = {
val errors1 =
List(
longitudeValidate(longitude)
, latitudeValidate(latitude)
)
val errors2 =
if (errors1.isEmpty)
List(
surfaceTypeValidate(longitude, latitude, surfaceType)
)
else Nil
val errorsFinal = (errors1 ::: errors2).flatten
if (errorsFinal.nonEmpty) Some(errorsFinal)
else None
}
def createEither(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Either[List[CaseClassValidationException], Surface3] =
fullValidate(longitude, latitude, surfaceType) match {
case Some(errors) => Left(errors)
case None => Right(new Surface3(longitude, latitude, surfaceType))
}
def createOption(longitude: Double = 0.0d, latitude: Double = 0.0d, surfaceType: SurfaceType.Value = SurfaceType.SEA): Option[Surface3] =
createEither(longitude, latitude, surfaceType) match {
case Right(surface3) => Some(surface3)
case Left(_) => None
}
}
case class Surface3 private (longitude: Double, latitude: Double, surfaceType: SurfaceType.Value)
//
val surface3option1a = Surface3.createOption()
val surface3option1b = Surface3.createOption(0.0d, 0.0d, SurfaceType.SEA)
val surface3option1c = Surface3.createOption(-180.0d, -90.0d, SurfaceType.ICE)
val surface3option1d = Surface3.createOption(-180.1d, -90.1d, SurfaceType.ICE) //None
val surface3either1a = Surface3.createEither()
val surface3either1b = Surface3.createEither(0.0d, 0.0d, SurfaceType.SEA)
val surface3either1c = Surface3.createEither(-180.0d, -90.0d, SurfaceType.ICE)
val surface3either1d = Surface3.createEither(-180.1d, -90.1d, SurfaceType.ICE) //Left[...]
【问题讨论】:
-
如果你不介意有额外的依赖,你应该看看 Scalaz 的 ValidationNel。看看这个post 了解更多信息。
-
现在,我宁愿找出没有 Scalaz 的最小样板解决方案。不过,Tysvm 的建议。
标签: scala validation installation case-class