Après la découverte des bases de la gestion des erreurs avec scala.util.Try, nous allons, dans cette deuxième partie, explorer des fonctionnalités plus avancées. Nous utiliserons un exemple très simple pour illustrer nos propos : lancer un serveur sur un numéro de port fourni par l'utilisateur. Cette tâche se décompose en deux sous-tâches, la première consiste à convertir l'entrée (input dans le code) de l'utilisateur en un numéro de port et la seconde à démarrer le serveur sur ce port. Commençons par la première :
val input = "..."
val result = Try { input.toInt }
Encapsulons ce bout de code dans une fonction
parsePort :
def parsePort(input: String): Try[Int] = Try(input.toInt)
Comme nous l'avons vu dans la première partie le type de retour de cette méthode est
scala.util.Success ou
scala.util.Failure selon que la valeur saisie par l'utilisateur soit un nombre ou non. La méthode ci-dessous teste notre super méthode :
@Test
def parsePortTest() {
assertTrue(parsePort("80").isInstanceOf[Success[_]])
assertTrue(parsePort("toto").isInstanceOf[Failure[_]])
}
Munis de cette méthode attaquons-nous à la deuxième sous-tâche : la transformation du numéro de port en un serveur sur ce port.
Transformer la valeur contenue dans Try
La méthode transform correspond à notre problématique. Voici sa signature :
def transform[U](s: (T) ⇒ Try[U], f: (Throwable) => Try[U]): Try[U]
C'est une fonction d'ordre supérieur qui prend deux paramètres :
Le type SimpleServer ci-dessus est :
case class SimpleServer(port: Int) {
def start(): Unit = {
println("Starting the server on port " + port)
...
}
...
}
Et enfin le test du code :
import org.junit.Test
import org.junit.Assert._
import scala.util.{Failure, Success, Try}
@Test
def transformTest() {
//Transform the result
def handleSuccess = (port: Int) ⇒ Try(SimpleServer(port))
def handleFailure = (t: Throwable) ⇒ Try(SimpleServer(80))
//Normal case : we create the server instance with the specified port
val normalCase = parsePort("9090").transform(handleSuccess, handleFailure)
assertEquals(Success(SimpleServer(9090)), normalCase)
//Erroneous case : we create the server instance with the default port
val erroneousCase = parsePort("toto") transform (handleSuccess, handleFailure)
assertEquals(Success(SimpleServer(80)), erroneousCase)
}
Fournir un traitement par défaut en cas d'échec
La méthode recover permet de spécifier une fonction dont l'invocation fournit une valeur dans le cas où l'instance du Try est un objet scala.util.Failure.
def recover[U >: T](f: PartialFunction[Throwable, U]): Try[U]
Vous aurez remarqué que la fonction en question est une fonction partielle, et nous la définissons comme suit en ne prenant en compte que les exceptions de type
NumberFormatException. Lorsqu'une telle exception est lancée nous utilisons le port 80.
import org.junit.Test
import org.junit.Assert._
import scala.util.{ Failure, Success, Try }
@Test
def recoverTest() {
val erroneousCase = parsePort {
"toto"
} recover {
case _: NumberFormatException ⇒ 80 // PartialFunction
} flatMap { port ⇒
Success(SimpleServer(port))
}
assertEquals(Success(SimpleServer(80)), erroneousCase)
}
So far so good ! Maintenant voyons comment appliquer un prédicat sur le contenu de Try.
Filtrer sur le contenu dans une instance de Try
Après avoir parsé le numéro renseigné par l'utilisateur nous devons d'abord vérifier qu'il n'est pas déjà utilisé par un autre service. La méthode filter sert à cela :
parsePort ("8080")
Nous filtrons le résultat obtenu avec la méthode filter de Try et le prédicat suivant :
def predicate(port: Int): Boolean = port != 8080
La méthode filter a la signature suivante :
def filter(p: (T) ⇒ Boolean): Try[T]
Elle prend un prédicat et transforme le Try en un objet de type Failure si le prédicat n'est pas satisfait. FilterTest résume tout cela :
@Test
def filterTest() {
val erroneousCase = parsePort {
"8080"
} filter {
predicate(_)
}
assertTrue(erroneousCase.isInstanceOf[Failure[_]])
}
No restons pas sur cet échec car la méthode
recover est là pour permettre de spécifier une valeur par défaut, par exemple 8081, dans le cas où le filtrage échoue.
@Test
def filterTest() {
val result = parsePort {
"8080"
} filter {
predicate _
} recover {
case _ ⇒ 8081
} map { port ⇒
SimpleServer(port)
}
assertEquals(Success(SimpleServer(8081)), result)
}
Ce code fait beaucoup de choses. D'abord il parse la chaine "8080" et obtient
Success(8080). Ensuite il transforme cet objet en un objet
Failure avec la méthode
filter car le prédicat n'est pas satisfait. La méthode
recover fournit 8081 comme valeur par défaut et enfin
map crée le serveur avec ce port.
Et pour la route
Ici prend fin notre découverte de scala.util.Try qui offre une API élégante pour gérer des traitements pouvant échouer. À ce point vous en savez assez pour aller plus loin.