Happy Path, Sad Path

13 octobre 2015

Cet article est la transcription d’une intervention donnée à ParisJS en octobre 2015, sur les liens entre la complexité métier et l’expérience utilisateur.

001

Aujourd’hui je voudrais vous parler de « comment gérer la complexité métier ». J’ai appelé ça « Happy Path et Sad Path », mais ça veut juste dire « Le chemin où tout se passe bien, et celui où on butte dans tous les cas spéciaux », et où on doit vraiment gérer la complexité.

Au début on présentait Captain Train en disant « c’est un site pour acheter ses billets de train, super rapidement ». Historiquement il a été conçu comme une alternative au site de l’opérateur historique : plus rapide, plus simple.

Aujourd’hui l’ambition est plus large : Captain Train vend des billets dans toute l’Europe. Mais on a toujours cette attention à la simplicité et à la rapidité.

La question est : comment conserver cette rapidité malgré la complexité ?

Je ne vais pas parler de performances chiffrées, mais plutôt de l’impression de simplicité. La frontière entre « rapide » et « simple » est très floue – et souvent, quand une tâche est simple, qu’on n’a pas le cerveau encombré de plein de choses, on va plus vite pour faire cette tâche. Et on en garde un meilleur souvenir.

Happy Path

Sur Captain Train, le Happy Path (celui où tout se passe bien pour l’utilisateur) est très rapide. Une dizaine de clics tout au plus pour acheter un billet de train.

En pratique ça se fait en cinq écrans (en comptant large) :

1 - La recherche 007

2 - La réservation 008

3 - Le panier 009

4 - Le paiement 010

5 - Le billet 011

Ça a l’air simple.

Sad Path

Mais tout ne se passe pas toujours aussi bien. Il y a des erreurs, des avertissements, des problèmes, de la complexité métier. C’est le cas dans tous les domaines que vous pouvez rencontrer : le train, la presse, la restauration, l’astronomie, la chirurgie, tous les domaines.

Pour continuer sur l’exemple du train, on va faire un tour de tout ce à quoi vous échappez la plupart du temps, mais qui peut arriver.

Pour commencer, au moment de la réservation, il y a beaucoup de choses qui peuvent se passer.

Par exemple, parfois le train ne part pas de la gare que vous avez demandée. (Ou il n’arrive pas à la gare demandée, c’est possible aussi.) Dans ce cas on vous prévient, quand même.

014

Jusqu’ici, c’est d’une complexité raisonnable.

Cette fois-ci vous décidez de prendre un train OUIGO (vous savez, les TGV low-cost). À bord des OUIGO, les bagages sont payants. Une cause fréquente de réclamations est que l’utilisateur monte dans le train avec trop de bagages.

Si vous n’avez pas sélectionné de bagages, on vous rapelle que vous ne pourrez avoir qu’un bagage « cabine ».

015

Pour faire une réservation, OUIGO requiert absolument la date de naissance des voyageurs.

Donc au moment où vous voulez réserver, si on n’a pas votre date de naissance, on vous la demande (et on s’en souvient pour les prochaines fois).

016

OUIGO requiert également le numéro de téléphone. Pareil, si on ne l’a pas on le demande, et on s’en souvient pour les prochaines fois.

017

J’ai séparé les dialogues pour la démo, mais en vrai si on a besoin de toutes ces informations en même temps, on va même compacter le processus, et les demander en une seule fois dans le même dialogue.

Et ça y est, c’est bon, on peut réserver.

Autre possibilité : vous réservez un train allemand, ou italien. Ça se passe encore un peu différemment. Les trains allemands et italiens demandent des documents d’identification, pour justifier de votre identité à bord du train, en plus du billet.

Donc avant de réserver, on vous demande si vous voulez utiliser votre carte bancaire pour vous identifier, ou votre carte d’identité.

Oh, et la Deutsche Bahn demande une adresse, aussi. Pareil, si on ne l’a pas, on la demande et on s’en souvient pour les prochaines fois.

019

Ah, et si vous choisissez de présenter votre carte d’identité, la DB demande le numéro de la carte. Et l’adresse indiquée sur la carte d’identité, aussi. Pareil, on la demande, on s’en souvient pour les prochaines fois.

020

On arrive enfin au Panier, juste avant le paiement. Là encore il peut se passer des choses.

Par exemple vous cliquez sur « Payer », mais vous avez déjà acheté un billet identique. C’est à dire un billet pour le même trajet, le même jour, avec les mêmes personnes qui voyagent.

On est sympa, on prévient, et on vérifie que c’est bien ça que vous voulez, quand même.

022

Autre problème qui peut arriver : vous allez payer deux fois le même billet.

Ça a l’air bête, mais ça peut arriver quand on fait une recherche, puis qu’on revient plus tard, on en fait une autre, on paye – et paf le billet de la recherche précédente était en fait là aussi.

Pareil, on vous demande de vérifier.

023

Si vous persistez, vous arrivez enfin au paiement.

Normalement ça se passe bien. Mais si votre banque nous demande de vous authentifier plus sérieusement, vous allez voir le décevant dialogue de confirmation 3D-Secure, celui qui vous envoie un SMS sur votre téléphone mobile.

Il y a pas mal d’abandons d’achat à cet étape, parce qu’il y a beaucoup de possibilités que ça ne fonctionne pas, d’une manière ou d’une autre. Ce dialogue joue donc sur plusieurs choses :

025

Ça y est, le paiement s’est bien passé. Vous avez votre billet.

Enfin presque. À ce stade, votre billet est payé, mais pas encore émis. On prétend que vous l’avez déjà, mais ce n’est pas complètement vrai, c’est « en cours ».

Par exemple, si on a vraiment des doutes sur votre paiement, malgré le 3D-Secure, on va jeter un œil manuellement, quand même, avant de vous générer un billet qui vaut de l’argent. Ça nous permet de lutter contre la fraude.

En tout cas on vous informe de ce qui se passe, et de combien de temps ça va prendre.

027

Une fois qu’on est raisonnablement confiant dans votre paiement, l’émission est lancée.

Normalement ça prend moins d’une minute – mais parfois les systèmes de réservation de la SNCF ou de la DB ont des hoquets, et il faut attendre quelques heures que ça revienne.

Alors pareil, on indique ce qui se passe.

028

On a réussi à émettre votre billet, mais catastrophe, le serveur qui permet de récupérer les e-billets en PDF est en rade.

Allez, on vous informe, quand même.

031

Ça y est, on a enfin récupéré votre e-billet en PDF.

Enfin vos billets : ici il y a deux passagers, et ce transporteur génère un PDF par passager. Donc deux PDF. Mais ça dépend.

033

Et puis parfois le mode de retrait est différent. Par exemple si vous prenez un train TER, le e-billet n’est souvent pas disponible. Dans ce cas il faut retirer un billet cartonné à une borne en gare. C’est inhabituel, donc on a intérêt à l’indiquer clairement.

035

Tout ça dans un message d’une seule ligne. Pour info, il y a plus d’un millier de combinaisons différentes de ce message. Ça dépend :

Contraintes métier

Toutes ces problématiques sont liées au train : ce sont nos contraintes métiers. Mais tout le monde a ses propres contraintes métier (même si ce n’est pas le métier du train).

Comment gérer la complexité qui vient des contraintes métier d’une manière générale ? Comment éviter de tout complexifier juste à cause de quelques contraintes métier ?

Déterminer ce qu’on voudrait faire est plutôt facile :

Ce sont des choses plutôt évidentes, nous voulons tous ça.

Éviter l’effet sapin-de-Noël

Mais si on n’y prend pas garde, on répond à des besoin évidents par des solutions évidentes : pour chaque confusion possible, rajouter un avertissement ; pour chaque cas spécial, rajouter un « if » dans le code.

Par exemple sur Captain Train on aurait pu mettre des dialogues systématiques pour :

Mais ça détériore l’expérience utilisateur, et ça rend le code de moins en moins maintenable.

Garder simple ce qui est simple

Ce qu’on veut vraiment, ce ne sont pas les solutions naïves, c’est garder simple ce qui est simple – et gérer au cas-par-cas quand ça se complique.

Pour rebondir sur ce dernier point, on essaie de résister à la tentation d’avoir des messages d’erreurs génériques, comme par exemple :

« Ce train ne part pas de la gare demandée. » 😕

À la place on utilise toutes les informations du contexte pour afficher un message pertinent : quelle était la gare demandée initialement, quelle est celle finalement remontée, et est-ce que c’est tout près ou vraiment loin :

« Ce train ne part pas de Paris, mais de Massy-Palaiseau, à 30km de Paris. » 😌

Autre exemple :

« Pour faire cette opération, contactez votre administrateur. » 😕

Allons bon, qui est mon « administrateur », déjà ? Et si j’arrive à le joindre, je lui demande d’effectuer quelle opération ?

« Pour ajouter un passager, contactez Delphine ou Jean-Luc. » 😌

Certains messages d’avertissement ou d’erreur sont générés à partir de 5 ou 10 bouts de phrases, et 50 lignes de code. Pour un seul message. C’est du travail, mais ça en vaut la peine.

Des promesses, toujours des promesses

Il y a quelques patterns qu’on utilise régulièrement pour améliorer l’expérience utilisateur :

Le premier pattern concerne le code. Une des manières de ne pas avoir à rajouter des « if » à chaque cas spécial est d’encapsuler les séquences de plusieurs dialogues dans des promesses.

Par exemple, quand on clique sur le bouton « Ajouter au Panier », on construit cette chaîne de promesses :

function addToCartClicked() {
  return luggagesDialog.prompt().then(() => {
    return birthdateDialog.prompt();
  }).then(() => {
    return identificationDialog.prompt();
  }).then(() => {
    return book();
  });
}

Si le dialogue n’a pas besoin d’être affiché, la promesse est résolue immédiatement, et on passe au dialogue suivant.

Par contre, s’il est nécessaire d’afficher un dialogue, la promesse sera résolue seulement au moment où l’utilisateur cliquera sur “OK”. Et si à une étape l’utilisateur clique sur “Annuler”, la promesse est rejetée, et toute les étapes suivantes sont court-circuitées.

Un autre exemple, quand on clique sur le bouton « Payer » :

function payButtonClicked() {
  return similarTicketDialog.prompt().then(() => {
    return duplicateTicketDialog.prompt();
  }).then(() => {
    return invalidCouponDialog.prompt();
  }).then(() => {
    return payDialog.prompt();
  });
}

Ça correspond exactement à notre cas d’usage : rajouter des dialogues optionnels sur le chemin, tout en maintenant le code lisible.

Tester les cas d’erreur en priorité

Les messages d’erreur sont les premiers à souffrir quand on refactore, ou que l’API change, etc.

Le Happy Path est le plus fréquent, il est utilisé en permanence : s’il casse, ça se verra forcément. Les cas d’erreurs, c’est plus compliqué. Finalement, pour les tests d’intégrations il vaut mieux tester les cas d’erreur en priorité. On n’est pas encore très bon élèves là dessus, mais on essaie.

visit('/pay');click('Payer');
andThen(function() {
  assert.present('Le paiement a été refusé');
});

Tests utilisateurs

Les tests utlisateurs nous ont permis de clarifier certains éléments de l’interface. Parce que épurer, c’est bien, mais parfois on a besoin de rendre les choses plus évidentes, au contraire.

Par exemple il a fallu qu’on aille dans un café faire des tests utilisateurs pour voir que les gens avaient du mal à trouver comment ajouter des passagers, ou comment ajouter une carte de réduction.

055

Alors on a rendu ça plus clair, sur le fond et sur la forme. D’abord en signalant un peu mieux l’ajout de passagers, avec un bouton « + », purement visuel, qui n’apporte rien fonctionnellement, si ce n’est un signal « Pour ajouter, c’est par ici. »

057

Ensuite en signalant mieux la possibilité d’ajouter des cartes de réductions.

059

Pour résumer ce qui peut vous être utile :

  1. Maitrisez vos contraintes métiers : ça permet de simplifier le cas le plus fréquent, et de demander des opérations supplémentaires au moment où vous en avez vraiment besoin.

  2. Une interface simple aura l’air rapide à l’utilisation, parce qu’elle diminue la charge cognitive. (Et pour moi c’est orthogonal aux performances brutes.)

Merci.

Kudos

Discussion, liens, et tweets

J’écris des sites web, des logiciels, des applications mobiles. Vous me trouverez essentiellement sur ce blog, mais aussi sur Mastodon, parmi les Codeurs en Liberté, ou en haut d’une colline du nord-est de Paris.