让我们说我有一个方法addUser
,可以将用户添加到数据库中。调用时,方法可能是:
该方法可能包含单个数据库API调用,如果失败则会抛出异常。如果它是普通的Java,我可能会在方法中捕获异常并检查原因。如果它属于第二类(无效输入),我会抛出一个自定义检查异常来解释原因(例如UserAlreadyExistsException
)。在第二类的情况下,我只是重新抛出原始异常。
我知道Java中有关于错误处理的强烈意见,因此可能会有人不同意这种方法,但我现在想关注Scala。
所述方法的优点是,当我调用addUser
时,我可以选择捕获UserAlreadyExistsException
并处理它(因为它适合我当前的抽象级别)但是同时我可以选择完全忽略可能抛出的任何其他低级数据库异常并让其他层处理它。
现在,我如何在Scala中实现同样的功能?或者什么是正确的Scala方法?显然,异常在Scala中的工作方式完全相同,但我发现有更好,更合适的方式。
据我所知,我可以使用Option
,Either
或Try
。然而,这些都不像优秀的旧例外那样优雅。
例如,处理Try
结果看起来像这样(从similar question借来):
addUser(user) match {
case Success(user) => Ok(user)
case Failure(t: PSQLException) if(e.getSQLState == "23505") => InternalServerError("Some sort of unique key violation..")
case Failure(t: PSQLException) => InternalServerError("Some sort of psql error..")
case Failure(_) => InternalServerError("Something else happened.. it was bad..")
}
最后两行正是我想要避免的,因为我必须将它们添加到我进行数据库查询的任何地方(并指望MatchError
似乎并不像一个好主意)。
同样处理多个错误源似乎有点麻烦:
(for {
u1 <- addUser(user1)
u2 <- addUser(user2)
u3 <- addUser(user3)
} yield {
(u1, u2, u3)
}) match {
case Success((u1, u2, u3)) => Ok(...)
case Failure(...) => ...
}
这比那更好:
try {
u1 = addUser(user1)
u2 = addUser(user2)
u3 = addUser(user3)
Ok(...)
} catch {
case (e: UserAlreadyExistsException) => ...
}
前者是否有任何我不知道的优势?
根据我的理解,Try
在不同线程之间传递异常时非常有用,但只要我在单个线程中工作,它就没有多大意义。
我想听听有关此事的一些论点和建议。
答案 0 :(得分:2)
这个话题的大部分当然是一个意见问题。不过,还有一些具体要点可以做出:
您认为Option
,Either
和Try
非常通用是正确的。名称不提供太多文件。因此,您可以考虑自定义密封特性:
sealed trait UserAddResult
case object UserAlreadyExists extends UserAddResult
case class UserSuccessfullyAdded(user: User) extends UserAddResult
这在功能上等同于Option[User]
,还有文档的额外好处。
Scala中的异常始终未选中。因此,您可以在Java中使用未经检查的异常的相同情况下使用它们,而不是在使用已检查异常的情况下使用它们。
有Try
,scalaz's Validation
或the monadic projections of Either
等monadic错误处理机制。
这些monadic工具的主要目的是让调用者一起组织和处理几个异常。因此,无论是在文档还是行为方面,让方法返回这些类型都没有太大的好处。任何想要使用Try
或Validation
的来电者都可以将您的方法的返回类型转换为所需的monadic形式。
正如你可以从我说出这些观点的方式猜测,我赞成定义自定义密封特征,因为这提供了最好的自我记录代码。但是,这是一个品味问题。