El recorrido
● Error y Excepción.
● El excepcionalismo imperativo.
● Encapsular excepciones en Try.
● Errores como parte del dominio.
● Manejo de errores con Scala Either.
● Acumular errores con Validated.
● Reportar errores y aciertos con Ior.
● Conclusiones.
Error y Excepción
● Error: resultado no deseado de la evaluación de un programa.
● Excepción: condición excepcional del programa en tiempo de
ejecución.
El excepcionalismo imperativo
● Durante la evaluación de un método, si se encuentra un error se arroja
una excepción.
● Este paradigma puede tener su origen en:
○ Lenguajes imperativos sin un sistema de tipos robusto.
○ Considerando lo anterior, carencia de tipos de datos (datatypes) que
permitan manejar los errores.
● Se requiere try/catch para manejar los métodos que puedan arrojar
excepciones.
El dominio
case class Book(isbn: String, title: String, author: String, genre: Genre)
trait Genre extends Product with Serializable
object Genre {
case object Fiction extends Genre
case object ScienceFiction extends Genre
case object HistoricNovel extends Genre
case object InvalidGenre extends Genre
}
val theFountainhead = Book("ISBN: 978-1-4028-9462-6","The Fountainhead","Ayn Rand",Genre.Fiction)
val atlasShrugged = Book("ISBN-13 978-1-4028-9462-6","Atlas Shrugged","Ayn Rand",Genre.Fiction)
val theCountOfMontecristo = Book("ISBN-13 978-1-4028-9462","The Count Of Montecristo","Alexandre
Dumas",Genre.HistoricNovel)
val titlelessBook = Book("ISBN-13 978-1-4028-9462-6","","Unknown",Genre.InvalidGenre)
Un libro con isbn, título, autor y género.
El excepcionalismo imperativo
private def validateGenre(g: Genre): Unit =
if ( g == Genre.InvalidGenre ) throw new InvalidParameter("Book has invalid genre")
private def validateIsbn(isbn: String): Unit = isbn match {
case isbnRegex(all @ _*) => ()
case _ => throw new InvalidParameter("isbn has not a valid format")
}
private def validateTitle(title: String): Unit =
if (title.isEmpty || title == null) throw new InvalidParameter("title must not be empty")
private def validateAuthor(author: String): Unit =
if (author.isEmpty || author == null) throw new InvalidParameter("author must not be empty")
class InvalidParameter(message: String) extends Exception(message)
class EmptyBookList(message: String) extends Exception(message)
El excepcionalismo imperativo
def validateBook(book: Book): Book = {
validateGenre(book.genre)
validateIsbn(book.isbn)
validateTitle(book.title)
validateAuthor(book.author)
book
}
def validateBooks(books: List[Book]): List[Book] =
if (books == Nil) throw new EmptyBookList("Book list was empty")
else {
val booksBuffer = new ListBuffer[Book]
for (book <- books) {
try booksBuffer += validateBook(book) catch { case ex: Exception => println(s"Error $ex") }
}
booksBuffer.toList
}
Encapsulando excepciones con Try
● El tipo Try[A] es utilizado para manejar excepciones en métodos.
● Encapsula el resultado en dos subtipos: Success y Failure.
● Success contiene el resultado del método.
● Failure contiene la excepción arrojada.
● Nos permite usar for comprehension.
● Es fail fast en Failure.
Encapsulando excepciones con Try
private def validateGenre(g: Genre): Try[Genre] = Try {
g match {
case Genre.InvalidGenre => throw new InvalidParameter("Book has invalid genre")
case genre => genre
}
}
private def validateIsbn(isbn: String): Try[String] = Try {
isbn match {
case isbnRegex(all @ _*) => isbn
case _ => throw new InvalidParameter("isbn has not a valid format")
}
}
private def validateTitle(title: String): Try[String] = Try {
if (Option(title).forall(_.isEmpty)) throw new InvalidParameter("title must not be empty") else title
}
private def validateAuthor(author: String): Try[String] = Try {
if (Option(author).forall(_.isEmpty)) throw new InvalidParameter("author must not be empty") else author
}
Encapsulando excepciones con Try
import cats.syntax.semigroup._
//Valida los libros y devuelve una lista no vacía en el caso Success
def validateBook(b: Book): Try[NonEmptyList[Book]] =
for {
i <- validateIsbn(b.isbn)
a <- validateAuthor(b.author)
t <- validateTitle(b.title)
g <- validateGenre(b.genre)
} yield NonEmptyList.of(Book(i, t, a, g))
//Combina los resultados exitosos del Try en una lista no vacía
def validateBooks(bs: List[Book]): Try[NonEmptyList[Book]] = bs match {
case Nil => Failure(new EmptyBookList("Book list was empty"))
case books => books map validateBook reduce (_ |+| _)
}
Encapsulando excepciones con Try
"BookValidation" should {
"Validate a book" in {
val validated = validateBook(theFountainhead)
validated should === (Success(NonEmptyList(theFountainhead, Nil)))
}
"Validate books" in {
val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))
validatedBooks should === (Success(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))
}
"Fail fast error on books" in {
val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))
inside(validatedBooks) { case Failure(ex) => ex.getMessage shouldBe "title must not be empty" }
}
}
Errores como parte del dominio
● Un error en una función no debe tratarse como algo excepcional.
● Excepciones para interacciones con la capa de infraestructura.
● Excepciones rompen con type safety.
● Los errores también son parte del dominio.
//No es cierto que validateBook siempre devuelve un Book
def validateBook(book: Book): Book = {
validateGenre(book.genre)
validateIsbn(book.isbn)
validateTitle(book.title)
validateAuthor(book.author)
book
}
Errores como parte del dominio
sealed trait Error extends Product with Serializable {
val message: String
}
case class InvalidParameter(message: String) extends Error
case class EmptyBookList(message: String) extends Error
Definimos las excepciones anteriormente usadas como ADTs de Error.
Manejo de errores con Scala Either
● El tipo Either[A, B] representa el valor de dos tipos posibles: Left o Right
● Convencionalmente se usa el Left para representar errores y Right para
representar los aciertos.
● Nos facilita manejar los errores modelados como parte del dominio.
● Se puede usar for comprehension.
● Es fail fast en Left.
Manejo de errores con Scala Either
private def validateGenre(g: Genre): Either[InvalidParameter, Genre] = g match {
case InvalidGenre => Left(InvalidParameter("Book has invalid genre"))
case genre => Right(genre)
}
private def validateIsbn(isbn: String): Either[InvalidParameter, String] = isbn match {
case isbnRegex(all @ _*) => Right(isbn)
case _ => Left(InvalidParameter("isbn has not a valid format"))
}
private def validateTitle(title: String): Either[InvalidParameter, String] =
if (Option(title).forall(_.isEmpty)) Left(InvalidParameter("title must not be empty")) else Right(title)
private def validateAuthor(author: String): Either[InvalidParameter, String] =
if (Option(author).forall(_.isEmpty)) Left(InvalidParameter("author must not be empty")) else Right(author)
Manejo de errores con Scala Either
import cats.syntax.semigroup._
//Valida secuencialmente todos los campos, se detiene en el primer error encontrado.
def validateBook(b: Book): Either[InvalidParameter, NonEmptyList[Book]] =
for {
i <- validateIsbn(b.isbn)
a <- validateAuthor(b.author)
t <- validateTitle(b.title)
g <- validateGenre(b.genre)
} yield NonEmptyList.of(Book(i, t, a, g))
//Por ser fail fast, reporta el primer error encontrado pero acumula todos los aciertos.
def validateBooks(bs: List[Book]): Either[Error, NonEmptyList[Book]] = bs match {
case Nil => Left(EmptyBookList("Book list was empty"))
case books => books map validateBook reduce (_ |+| _)
}
Manejo de errores con Scala Either
"BookValidation" should {
"Validate a book" in {
val validated = validateBook(theFountainhead)
validated should === (Right(NonEmptyList(theFountainhead, Nil)))
}
"Validate books" in {
val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))
validatedBooks should === (Right(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))
}
"Fail fast error on books" in {
val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))
validatedBooks should === (Left(InvalidParameter("title must not be empty")))
}
}
Manejo de errores con Cats Validated
● El tipo Validated[A, B] representa el valor de dos tipos posibles: Valid e
Invalid.
● Desde el punto de vista semántico, es obvio que Invalid es utilizado para
representar los errores y Valid los aciertos.
● Nos facilita manejar los errores modelados como parte del dominio.
● No se puede utilizar en for comprehension.
● Acumula errores en Invalid ( type ValidatedNel[A, B] =
Validated[NonEmptyList[A], B] ).
Manejo de errores con Cats Validated
private def validateGenre(g: Genre): ValidatedNel[InvalidParameter, Genre] = g match {
case InvalidGenre => InvalidParameter("Book has invalid genre").invalidNel
case genre => genre.validNel
}
private def validateIsbn(isbn: String): ValidatedNel[InvalidParameter, String] = isbn match {
case isbnRegex(all @ _*) => isbn.validNel
case _ => InvalidParameter("isbn has not a valid format").invalidNel
}
private def validateTitle(title: String): ValidatedNel[InvalidParameter, String] =
if (Option(title).exists(_.isEmpty)) InvalidParameter("title must not be empty").invalidNel else title.validNel
private def validateAuthor(author: String): ValidatedNel[InvalidParameter, String] =
if (Option(author).exists(_.isEmpty)) InvalidParameter("author must not be empty").invalidNel
else author.validNel
Manejo de errores con Cats Validated
import cats.syntax.cartesian._
import cats.syntax.semigroup._
//Valida en el producto cartesiano, acumula los errores en cada validación.
def validateBook(b: Book): ValidatedNel[InvalidParameter, NonEmptyList[Book]] = (
validateIsbn(b.isbn) |@|
validateAuthor(b.author) |@|
validateTitle(b.title) |@|
validateGenre(b.genre) ) map {
case (isbn, author, title, genre) =>
NonEmptyList.of(Book(isbn, title, author, genre))
}
//Puede acumular todos los errores o acumular todos los aciertos.
def validateBooks(bs: List[Book]): ValidatedNel[Error, NonEmptyList[Book]] = bs match {
case Nil => EmptyBookList("Book list was empty").invalidNel
case books => books map validateBook reduce (_ |+| _)
}
Manejo de errores con Cats Validated
"BookValidation" should {
"Validate books" in {
val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))
validatedBooks should === (Valid(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))
}
"Accumulate errors on books" in {
val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead))
validatedBooks should === (
Invalid(
NonEmptyList(InvalidParameter("title must not be empty"),
InvalidParameter("Book has invalid genre") :: InvalidParameter("isbn has not a valid format") :: Nil)
)
)
}
}
Manejo de errores con Cats Ior
● El tipo Ior[A, B] representa el valor de tres tipos posibles: Ior.Right,
Ior.Left e Ior.Both.
● La convención con Ior.Left e Ior.Right es igual a la de Either con respecto
a la representación de errores y aciertos.
● Ior.Both permite incluir simultáneamente los errores y aciertos
● Nos facilita manejar los errores modelados como parte del dominio.
● Se puede utilizar en for comprehension.
● Es fail fast en Ior.Left y acumula errores en Ior.Both.
Manejo de errores con Cats Ior
private def validateGenre(g: Genre): IorNel[InvalidParameter, Genre] = g match {
case InvalidGenre => Ior.left(NonEmptyList.of(InvalidParameter("Book has invalid genre")))
case genre => Ior.right(genre)
}
private def validateIsbn(isbn: String): IorNel[InvalidParameter, String] = isbn match {
case isbnRegex(all @ _*) => Ior.right(isbn)
case _ => Ior.left(NonEmptyList.of(InvalidParameter("isbn has not a valid format")))
}
private def validateTitle(title: String): IorNel[InvalidParameter, String] =
if (Option(title).exists(_.isEmpty)) Ior.left(NonEmptyList.of(InvalidParameter("title must not be empty")))
else Ior.right(title)
private def validateAuthor(author: String): IorNel[InvalidParameter, String] =
if (Option(author).exists(_.isEmpty)) Ior.left(NonEmptyList.of(InvalidParameter("author must not be empty")))
else Ior.right(author)
Manejo de errores con Cats Ior
import cats.syntax.semigroup._
//Valida los campos secuencialmente, se detiene en el primer error.
def validateBook(b: Book): IorNel[InvalidParameter, NonEmptyList[Book]] =
for {
i <- validateIsbn(b.isbn)
a <- validateAuthor(b.author)
t <- validateTitle(b.title)
g <- validateGenre(b.genre)
} yield NonEmptyList.of(Book(i, t, a, g))
//A diferencia de Validated, puede acumular errores y aciertos en Ior.Both
def validateBooks(bs: List[Book]): IorNel[Error, NonEmptyList[Book]] = bs match {
case Nil => Ior.left(EmptyBookList("Book list was empty"))
case books => books map validateBook reduce (_ |+| _)
}
Manejo de errores con Cats Ior
"BookValidation" should {
"Validate books" in {
val validatedBooks = validateBooks(List(theFountainhead, atlasShrugged))
validatedBooks should === (Ior.Right(NonEmptyList(theFountainhead, atlasShrugged :: Nil)))
}
"Fail fast error on books" in {
val validatedBooks = validateBooks(List(titlelessBook, theCountOfMontecristo, theFountainhead, atlasShrugged))
validatedBooks should === (
Ior.Both(
NonEmptyList(InvalidParameter("title must not be empty"),InvalidParameter("isbn has not a valid format") :: Nil),
NonEmptyList(theFountainhead, atlasShrugged :: Nil)
)
)
}
}
Conclusiones
● En una función no se deben arrojar excepciones, se pierde transparencia
referencial.
● Se favorece la modelación de errores en el dominio.
● Try es fail fast, no es expresivo en cuanto a los errores que encapsula y
se puede usar cuando se depende de métodos de librerías java.
● Either es fail fast y puede acumular aciertos.
● Validated acumula errores, no se puede usar en for comprehensions y
puede acumular aciertos.
● Ior es el más flexible: es fail fast en Ior.Left, acumula errores en Ior.Both y
puede acumular aciertos en Ior.Right e Ior.Both.
Top Related