Scala-way处理for-comprehensions的条件?

时间:2014-06-06 12:16:19

标签: scala exception-handling future

我正在努力创建一个整洁的结构,以便对基于未来的业务逻辑进行理解。以下示例包含基于异常处理的工作示例:

(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;)。因此,当您从单个常见错误类型派生它们时的方法可能不起作用。

3 个答案:

答案 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))
}

这里的想法是使用flatMapOptions中包含Future的内容转换为失败的Future None {{1}} 。对于comp来说,有很多方法可以清理它,这是一种可行的方法。

答案 2 :(得分:5)

主要的挑战是 for-comprehensions 一次只能在一个monad上工作,在这种情况下它是Future monad并且是短路序列的唯一方法未来的电话是为了将来失败。这是有效的,因为 for-comprehension 中的后续调用只是mapflatmap调用,以及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))
  }
}

对于每个调用,该操作实际上是一个潜在的操作,因为调用它或使用终止结果,将简单地保持终止结果。请注意,TerminatorResult[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

相关问题