服务 class 和日志记录设计 Scala with Futures

Service class and logging design Scala with Futures

我有一项服务 class 从数据库中获取一些数据(对于上下文,我使用的是 Play! Framework)。这是一个示例方法:

  def getAccessToken(id: BSONObjectID): Future[Option[String]] = {
    userDAO.find(id).map {
      case Some(user) =>
        user.settings flatMap (_.accessToken)
      case _ => None
    }
  }

我正在努力改进这件事的错误处理(Scala 的新功能),因为有几件事可能会出错:

  1. 可能找不到用户
  2. 可能会找到用户,但可能不会设置 accessTokenaccessTokenOption[String]

就目前情况而言,我无法区分两者。我自然倾向于使用 Scalaz 中的 \/ 并将 return 类型设为 Future[ErrorType \/ String],这似乎是一种合理的方法。在我的控制器方法中,我可以通过提升到包装器 monad 来理解一堆不同的服务方法。

但我有以下问题:

  1. 我的 ErrorType 应该扩展 Exception,还是我应该只使用密封特征样式并从中扩展。我听说在 Scala 中使用异常不是好的做法,所以我不确定正确的方法是什么。

  2. 如何在不使用过多的日志语句污染控制器 class 的情况下处理日志记录?如果一个控制器 class 调用了一堆这样的服务方法,那么控制器将不得不在 for comprehension 中处理几个不同的 ErrorType。假设我用 ?| 将所有 monad 提升到包装器 monad,我想避免这种情况:

    accessToken <- service.getAccessToken(id) ?| { error => error match { case Error1 =>
                   logger.error(
                     "Unable to find access token for user: " + id
                       .toString())
                   InternalServerError(
                     ApiResponse("internal_server_error",
                                 "Unable to get token."))
                   case Error2 => ...
                 }
    

谢谢!

我认为 Future[ErrorType / String] 有点矫枉过正,因为 Future[T] 已经可以容纳 T 类型的对象或 Exception 派生的对象(参见 Future.successful(...)/ Future.failed(...))

Should my ErrorType extend Exception, or should I just use the sealed trait style and just extend from that. I've heard that it is not good practice to use exceptions in Scala, so I'm not sure what the right approach is.

我建议使用一个 class(或一组 classes,每个特定错误类型一个),比​​如从 Exception 派生的 YourAppException,因为您需要处理低级异常无论如何。

我同意 throwing/catching 异常在函数代码中的表现不是很好,最好使用 Try[T] 或 Future[T] 来 return 以更明确的方式代替错误。另一方面,使用派生的异常 class 来保存一些错误信息并没有错。将原始的非应用程序(例如 IO)异常包装在应用程序异常中并在异常的 'cause' 中保留对初始异常的引用以进行故障排除通常很有用。它提供了提供更多上下文特定错误消息的机会。

How can I handle logging without polluting the controller class with excessive log statements?

考虑在 Exception 派生案例 classes 中封装错误消息,表示应用程序错误,以便您可以使用 exception.getMessage 统一访问错误消息。也可以很容易地向 YourAppException 添加一些方法来构造 ApiResponse。

def getAccessToken(id: BSONObjectID): Future[String] = {
    userDAO.find(id).flatMap {
      case Some(user) =>
        val optToken = user.settings.flatMap (_.accessToken)
        optToken.map(Future.successful).getOrElse(Future.failed(AccessTokenIsInvalid(user)))
      case _ => Future.failed(UserNotFoundError(user))
    }
  }

case class AccessTokenIsInvalid(user: String) 
  extends YourAppException(s"Access token is invalid for user $user") {
}

accessToken <- service.getAccessToken(id) ?| { error => 
           logger.error(error.getMessage)
           InternalServerError(
             ApiResponse("internal_server_error", error.getMessage))
         }

1) 是的,你走对了。异常的问题在于,当某事失败时,很难对其进行模式匹配以检测原因。 我会这样做:

sealed trait MyError
object UserNotFound extends MyError
object AuthFailed extends MyError

type MyResult = Either[MyError, String]

2) 如果程序类型正确,则在信息丢失的地方需要进行日志记录。 例如,如果您处理 val x = Future[Either[Error, String]],那么您还没有限制潜在的错误,因此日志记录是可选的。 但是,当您以某种方式尝试从中提取 Either[MyError, String] 时,您会丢失信息,因此您应该记录它。 当您从 Either[MyError, String].

中提取 String 时,也会发生同样的情况