我正在努力创建一个整洁的结构,以便对基于未来的业务逻辑进行理解。以下示例包含基于异常处理的工作示例:
(for {
// find the user by id, findUser(id) returns Future[Option[User]]
userOpt <- userDao.findUser(userId)
_ = if (!userOpt.isDefined) throw new EntityNotFoundException(classOf[User], userId)
user = userOpt.get
// authenticate it, authenticate(user) returns Future[AuthResult]
authResult <- userDao.authenticate(user)
_ = if (!authResult.ok) throw new AuthFailedException(userId)
// find the good owned by the user, findGood(id) returns Future[Option[Good]]
goodOpt <- goodDao.findGood(goodId)
_ = if (!good.isDefined) throw new EntityNotFoundException(classOf[Good], goodId)
good = goodOpt.get
// check ownership for the user, checkOwnership(user, good) returns Future[Boolean]
ownership <- goodDao.checkOwnership(user, good)
if (!ownership) throw new OwnershipException(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
})
.recover {
case ex: EntityNotFoundException =>
/// ... handle error cases ...
renderJson(Map(
"success" -> false,
"error" -> "Your blahblahblah was not found in our database"
))
case ex: AuthFailedException =>
/// ... handle error cases ...
case ex: OwnershipException =>
/// ... handle error cases ...
}
然而,这可能被视为处理事物的非功能性或非Scala方式。有更好的方法吗?
请注意,这些错误来自不同的来源 - 有些属于业务级别(&#39;检查所有权&#39;),有些属于控制器级别(&#39;授权&#39;),有些属于db级别(&#39;未找到实体&#39;)。因此,当您从单个常见错误类型派生它们时的方法可能不起作用。
答案 0 :(得分:8)
不要对预期行为使用例外。
它在Java中并不好用,而且它在 在Scala中并不好看。请see this question获取有关为何应避免使用常规控制流异常的详细信息。 Scala非常适合避免使用例外:您可以使用Either
s。
诀窍是定义您可能遇到的一些失败,并将Option
转换为包含这些失败的Either
。
// Failures.scala
object Failures {
sealed trait Failure
// Four types of possible failures here
case object UserNotFound extends Failure
case object NotAuthenticated extends Failure
case object GoodNotFound extends Failure
case object NoOwnership extends Failure
// Put other errors here...
// Converts options into Eithers for you
implicit class opt2either[A](opt: Option[A]) {
def withFailure(f: Failure) = opt.fold(Left(f))(a => Right(a))
}
}
使用这些助手,您可以使您的理解可读且无异常:
import Failures._
// Helper function to make ownership checking more readable in the for comprehension
def checkGood(user: User, good: Good) = {
if(checkOwnership(user, good))
Right(good)
else
Left(NoOwnership)
}
// First create the JSON
val resultFuture: Future[Either[Failure, JsResult]] = for {
userRes <- userDao.findUser(userId)
user <- userRes.withFailure(UserNotFound).right
authRes <- userDao.authenticate(user)
auth <- authRes.withFailure(NotAuthenticated).right
goodRes <- goodDao.findGood(goodId)
good <- goodRes.withFailure(GoodNotFound).right
checkedGood <- checkGood(user, good).right
} yield renderJson(Map("success" -> true)))
// Check result and handle any failures
resultFuture.map { result =>
result match {
case Right(json) => json // serve json
case Left(failure) => failure match {
case UserNotFound => // Handle errors
case NotAuthenticated =>
case GoodNotFound =>
case NoOwnership =>
case _ =>
}
}
}
答案 1 :(得分:7)
你可以稍微清理一下这个理解:
for {
user <- findUser(userId)
authResult <- authUser(user)
good <- findGood(goodId)
_ <- checkOwnership(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
}
假设这些方法:
def findUser(id:Long) = find(id, userDao.findUser)
def findGood(id:Long) = find(id, goodDao.findGood)
def find[T:ClassTag](id:Long, f:Long => Future[Option[T]]) = {
f(id).flatMap{
case None => Future.failed(new EntityNotFoundException(implicitly[ClassTag[T]].runtimeClass, id))
case Some(entity) => Future.successful(entity)
}
}
def authUser(user:User) = {
userDao.authenticate(user).flatMap{
case result if result.ok => Future.failed(new AuthFailedException(userId))
case result => Future.successful(result)
}
}
def checkOwnership(user:User, good:Good):Future[Boolean] = {
val someCondition = true //real logic for ownership check goes here
if (someCondition) Future.successful(true)
else Future.failed(new OwnershipException(user, good))
}
这里的想法是使用flatMap
将Options
中包含Future
的内容转换为失败的Future
None
{{1}} 。对于comp来说,有很多方法可以清理它,这是一种可行的方法。
答案 2 :(得分:5)
主要的挑战是 for-comprehensions 一次只能在一个monad上工作,在这种情况下它是Future
monad并且是短路序列的唯一方法未来的电话是为了将来失败。这是有效的,因为 for-comprehension 中的后续调用只是map
和flatmap
调用,以及map
/ flatmap
的行为失败的Future
将返回该未来并且不执行提供的主体(即被调用的函数)。
您要实现的目标是根据某些条件对工作流程进行简短的调整,而不是通过未来的失败来实现。这可以通过将结果包装在另一个容器中来完成,让我们称之为Result[A]
,这使得理解类型为Future[Result[A]]
。 Result
将包含结果值,或者是终止结果。挑战在于如何:
Result
Result
正在终止 map/flatmap
似乎是执行这些类型合成的候选人,除了我们必须手动调用它们,因为 for-comprehension 可以评估的唯一map/flatmap
是导致Future[Result[A]]
。
Result
可以定义为:
trait Result[+A] {
// the intermediate Result
def value: A
// convert this result into a final result based on another result
def given[B](other: Result[B]): Result[A] = other match {
case x: Terminator => x
case v => this
}
// replace the value of this result with the provided one
def apply[B](v: B): Result[B]
// replace the current result with one based on function call
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]]
// create a new result using the value of both
def combine[B](other: Result[B]): Result[(A, B)] = other match {
case x: Terminator => x
case b => Successful((value, b.value))
}
}
对于每个调用,该操作实际上是一个潜在的操作,因为调用它或使用终止结果,将简单地保持终止结果。请注意,Terminator
是Result[Nothing]
,因为它永远不会包含值,任何Result[+A]
都可以是Result[Nothing]
。
终止结果定义为:
sealed trait Terminator extends Result[Nothing] {
val value = throw new IllegalStateException()
// The terminator will always short-circuit and return itself as
// the success rather than execute the provided block, thus
// propagating the terminating result
def flatMap[A2 >: Nothing, B](f: A2 => Future[Result[B]]): Future[Result[B]] =
Future.successful(this)
// if we apply just a value to a Terminator the result is always the Terminator
def apply[B](v: B): Result[B] = this
// this apply is a convenience function for returning this terminator
// or a successful value if the input has some value
def apply[A](opt: Option[A]) = opt match {
case None => this
case Some(v) => Successful[A](v)
}
// this apply is a convenience function for returning this terminator or
// a UnitResult
def apply(bool: Boolean): Result[Unit] = if (bool) UnitResult else this
}
当我们已经满足终止条件时,终止结果可以短暂调用需要值[A]
的函数。
非终止结果定义为:
trait SuccessfulResult[+A] extends Result[A] {
def apply[B](v: B): Result[B] = Successful(v)
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]] = f(value)
}
case class Successful[+A](value: A) extends SuccessfulResult[A]
case object UnitResult extends SuccessfulResult[Unit] {
val value = {}
}
非终止结果可以为函数提供包含的值[A]
。为了更好地衡量,我还预定了UnitResult
用于纯粹副作用的函数,例如goodDao.removeGood
。
现在让我们定义您的好处,但终止条件:
case object UserNotFound extends Terminator
case object NotAuthenticated extends Terminator
case object GoodNotFound extends Terminator
case object NoOwnership extends Terminator
现在我们有了创建您正在寻找的工作流程的工具。每个complenhention想要一个在右侧返回Future[Result[A]]
的函数,在左侧生成Result[A]
。 flatMap
上的Result[A]
可以调用(或短路)需要[A]
作为输入的函数,然后我们可以map
将其结果发送到新{ {1}}:
Result
我知道这很多设置,但至少def renderJson(data: Map[Any, Any]): JsResult = ???
def renderError(message: String): JsResult = ???
val resultFuture = for {
// apply UserNotFound to the Option to conver it into Result[User] or UserNotFound
userResult <- userDao.findUser(userId).map(UserNotFound(_))
// apply NotAuthenticated to AuthResult.ok to create a UnitResult or NotAuthenticated
authResult <- userResult.flatMap(user => userDao.authenticate(user).map(x => NotAuthenticated(x.ok)))
goodResult <- authResult.flatMap(_ => goodDao.findGood(goodId).map(GoodNotFound(_)))
// combine user and good, so we can feed it into checkOwnership
comboResult = userResult.combine(goodResult)
ownershipResult <- goodResult.flatMap { case (user, good) => goodDao.checkOwnership(user, good).map(NoOwnership(_))}
// in order to call removeGood with a good value, we take the original
// good result and potentially convert it to a Terminator based on
// ownershipResult via .given
_ <- goodResult.given(ownershipResult).flatMap(good => goodDao.removeGood(good).map(x => UnitResult))
} yield {
// ownership was the last result we cared about, so we apply the output
// to it to create a Future[Result[JsResult]] or some Terminator
ownershipResult(renderJson(Map(
"success" -> true
)))
}
// now we can map Result into its value or some other value based on the Terminator
val jsFuture = resultFuture.map {
case UserNotFound => renderError("User not found")
case NotAuthenticated => renderError("User not authenticated")
case GoodNotFound => renderError("Good not found")
case NoOwnership => renderError("No ownership")
case x => x.value
}
类型可以用于具有终止条件的任何Result
for-comprehension 。