问题
As mentioned in the title, does it make sense to use such data structure? Let me explain one by one:
- Future - to represent async computation
- Either - to communicate known errors
- Option - to communicate that the value may not be present
I am a little bit scared when looking at this. Is it a good practice to use such type combination?
回答1:
Let's have a look at the solution space:
Success(Right(Some(user))) => Everythig OK, got an user
Success(Right(None)) => Everything OK, no user
Success(Left(AppError)) => Something went wrong at app level
Failure(Exception) => Something went wrong
This looks very expressive, but things get ugly fast when you try to compose such nested structure with other calls (see Converting blocking code to using scala futures) for an example of composingFuture[Option[T]]
)
So following the principle of the least power,
we ask ourselves: Are there less complex alternatives that preserve the semantics?
One could argue that Future[User]
could be sufficient if we make use of the full potential of exception (and exception hierarchies).
Let's check:
Everythig OK, got an user => Success(user)
Everything OK, no user => Failure(UserNotFoundException) (Application level exception)
Something went wrong at app level => Failure(AppException) (Application level exception)
Something went wrong => Failure(Exception) (System-level exception)
The only limitation of this approach is that the users of the API will need to be aware of the Exceptions, which is not self-documented in the interface. The upper-hand is that having an API based on Future
s will allow expressive monadic compositions with other Future
-based APIs.
回答2:
Generally, there is nothing wrong with the proposed API. It gives you exactly the flexibility you need, but requires you to either write a decent amount of boilerplate to deal with the return type, or use scalaz/cats and monadic transformations to extract everything.
But, Let me try and propose an additional API.
Let's define our algebra (or abstract data types):
// parten me for the terrible name
sealed trait DomainEntity
case class User(id: UserId) extends DomainEntity
case object EmptyUser extends DomainEntity
case class UserId(id: String)
Instead of modeling the non-existence of a user with an Option[A]
, we use our algebra to define our domain.
Now, we can expose a Future[Try[DomainEntity]]
, which we can later match for different combinations generated by the API:
findUserById(UserId("id")).map {
case Success(user: User) => // Do stuff with user
case Success(EmptyUser) => // We have no user, do something else
case Failure(e) => // Log exception?
}
回答3:
Things like Future[Either[AppError, Option[User]]]
return type can be ok at the time of prototyping things but once you are done with prototyping you should think of options which offer better readability and expressibility.
Lets take this Future[Either[AppError, Option[User]]]
as an example. Lets say there is a method which has this return type.
def fetchUser(userId: UUID): Future[Either[AppError, Option[User]]]
Now, you can either choose to create a more expressive type hierarchy... for example,
// Disclamer :
// this is just for pointing you out towards a direction and
// I am sure many can propose a better design hierarchy
trait Model
case class User(id: UUID,....) extends Model
// Fetch Result protocol
sealed trait FetchModelResult
case class FetchModelSuccess(model: Model) extends FetchModelResult
sealed trait FetchModelFailure extends FetchModelResult
case class ModelNotFound extends FetchModelFailure
...
case class FetchModelGenericFailure(ex: Exception) extends FetchModelFailure
// App Result protocol
sealed trait AppResult
case class AppResultSuccess[T](result: T) extends AppResult
sealed trait AppResultFailure extends AppResult
case class AppResultGenericFailure(ex: Exception) extends AppResultFailure
// fetch user problem
def fetchUser(userId: UUID): Future[FetchModelResult] = ???
// Notice that we are not using the generic AppError here
// This is called segregation of problems
// the current problem is fetching the user
// so our design is just to represent what can happen while fetching
// Now whichever method is using this can come-up with an AppError
// or AppResult based on what is gets from here.
def fetchUserApiHandler(userId: UUID): Future[AppResult] =
fetchUser(userId).map({
case FetchModelSuccess(model) => .....
case FetchModelFailure(ex) => ....
})
The other option will be to use monadic composition and transformation utilities from scalaz
or cats
.
Raúl Raja Martínez has addressed similar problems and few ways to counter these in one of his presentations - A team's journey over Scala's FP emerging patterns - Run Wild Run Free
来源:https://stackoverflow.com/questions/40283042/futureeitherapperror-optionuser-in-scala