Blog Arolla

La gestion des erreurs avec scala.util.Try (2/2)

Suite de l'épisode précédent.

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 :

1
2
val input = "..."
val result = Try { input.toInt }

Encapsulons ce bout de code dans une fonction parsePort:

1
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.Failureselon que la valeur saisie par l'utilisateur soit un nombre ou non. La méthode ci-dessous teste notre super méthode :

1
2
3
4
5
@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 :

1
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 :

  • une fonction squi s'applique lorsque le traitement dans Try a réussi. Dans notre cas cette fonction est définie comme suit :
    1
    def handleSuccess = (port: Int) ⇒ Try(SimpleServer(port))
  • une fonction f qui s'applique lorsque le traitement a échoué. Nous utiliserons 80 comme numéro de port par défaut en cas d'erreur. Voici comment on la définit :
    1
    def handleFailure = (t: Throwable) ⇒ Try(SimpleServer(80))

Le type SimpleServer ci-dessus est :

1
2
3
4
5
6
7
case class SimpleServer(port: Int) {
 def start(): Unit = {
   println("Starting the server on port " + port)
   ...
 }
 ...
}

Et enfin le test du code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 :

1
parsePort ("8080")

Nous filtrons le résultat obtenu avec la méthode filter de Try et le prédicat suivant :

1
def predicate(port: Int): Boolean = port != 8080

La méthode filter a la signature suivante :

1
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 :

1
2
3
4
5
6
7
8
9
@Test
def filterTest() {
  val erroneousCase = parsePort {
    "8080"
  } filter {
    predicate(_)
  }
  assertTrue(erroneousCase.isInstanceOf[Failure[_]])
}

No restons pas sur cet échec car la méthode recoverest là pour permettre de spécifier une valeur par défaut, par exemple 8081, dans le cas où le filtrage échoue.

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 mapcré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.

Plus de publications