📐 Construire une app en JS
10 Application

L'Application

Arrivés au 10è chapitre, il est temps d'écrire le code de notre application.

Ou plus précisémment, le code qui fera passer au vert notre test :

// Injection des dépendances
const app = new App(testDependencies())
 
// Réalisation de la location
await app.run([
    login({email:"faketenant@mail.com", password:"secret"}),
    book({
        accomodationId:"accomodation-1", 
        adults:2, children:3, 
        from : new Date("2024-06-02"),
        to :   new Date("2024-06-02"),
        })
]);
 
// Le calendrier comporte la réservation
const bookings = await app.dependencies.listBookingsForAccomodationId("accomodation-1")
expect(bookings).toHaveLength(1)

Il y a seulement 10 lignes de code dans ce test. Pourtant, il est déjà très expressif et très structurant ! Si un autre développeur lit ce test, il comprend tout de suite votre intention.

Ecrire le test avant le code oblige à faire du code facile à utiliser et testable.

Recensons ce qu'il nous faut coder pour passer le test :

  • la classe App
  • l'objet testDependencies
  • la fonction login()
  • la fonction book()

Par quoi commencer ?

La réponse est souvent la même : d'abord les comportements. Le coeur de métier.

App et testDependencies sont des outils techniques pour faire marcher notre application.

login() est un comportement. Mas pas spécifique à un système de réservation d'hébergement.

Le comportement clé pour notre métier est book(). C'est la raison d'être de notre système.

Le typage fort

Vous brulez d'impatience de coder cette fonction book().

Respirons et typons.

Typer, c'est définir le type de la fonction. Que prend-elle en paramètres ? Que retourne-t-elle ?

PS : le typage est décrit avec un langage dédié à l'analyse statique : TypeScript. Je ne vais pas expliquer TypeScript, ce n'est pas l'objet de ce livre.

  1. Les entrées

De quoi book() a-t-elle besoin ?

  • de lire et de modifier l'état du système, au travers des dépendances
  • de lire le contexte d'exécution de la commande, pour connaître l'utilisateur connecté
  • des paramètres de la réservation (que l'on nomme payload)
  1. Les sorties

Que retourne la fonction book() ?

Une seule chose : le contexte éventuellement modifié, pour le passer à la commande suivante.

Alors on se lance :

function book(dependencies, context, payload) {
    // Do stuff
 
    return context
}

Un peu de curry

Est-ce que ça fonctionne avec notre test ?

Non.

Dans notre test, la commande book() n'a qu'un seul paramètre décrivant les caractéristiques de la réservation.

book({
    accomodationId:"accomodation-1", 
    adults:2, children:3, 
    from : new Date("2024-06-02"),
    to :   new Date("2024-06-02"),
    })

Ce sera tentant de le modifier, pour ajouter ce qui nous manque. Surtout pas ! Notre code doit de plier aux contraintes du test. Pas l'inverse.

Pour passer 3 paramètres en 1 seul, nous allons utiliser une technique de currying.

Elle consiste à découper notre fonction en 2 parties, dont la première retourne une fonction.

Et justement JavaScript nous aide dans cet exercice, puisque les fonctions sont des citoyens de premières classe [faire le lien ici].

function book(payload) {
    const {accomodationId,adults,children,from,to} = payload;
    return function (dependencies, context) {
        // Book accomodationId for the family from <from> to <to> 
 
        return context
    }
}

La commande retourne une fonction qui retourne le contexte modifié.

Alors on peut faire pareil pour la commande login(). Le payload sera un objet différent, comportant l'email et le mot de passe.

Mais elle retourne elle aussi une fonction qui retourne le contexte modifié.

function login(payload) {
    const {email, password} = payload;
    return function (dependencies, context) {
        // Check <email> and <password>
 
        return context
    }
}

Gestion des erreurs

Et si une commande est en erreur ?

Par exemple:

  • le login ne correspond pas à un utilisateur enregistré
  • le mot de passe est incorrect
function login(payload) {
    const {email, password} = payload;
    return function (dependencies, context) {
        const user = await dependencies.users.findByEmail(email);
        if (!user) {
            // Utilisateur inconnu
            return context.withError(UnknownUserEmail(email))
        }
 
        if (user.encryptedPassword !== encrypt(password)) {
            // Erreur de mot de passe
            return context.withError(WrongPassword(user))
        }
 
        // Tout est OK, ajoutons l'utilisateur au contexte
        // pour les commandes suivantes qui en auront besoin
        return context.withUser(user)
    }
}

Wooh, voici le code de notre commande login() qui s'écrit natuellement.

Pour retourner l'erreur, nous avons fait des hypothèses sur l'API du contexte:

  • il dispose d'une méthode withError() permettant d'écrire une erreur
  • et une méthode withUser() pour écrire l'utilisateur connecté

Savourons cette liberté d'utiliser des méthodes qui n'existent pas encore.

En commençant par les tests, puis par les commandes, on écrit le code tel qu'il devrait être.

Alors exploitons cela à fond, en définissant des fonctions UnknownUserEmail(email) et WrongPassword(user) qui construisent l'erreur à ajouter au contexte.

Ainsi, à la lecture du code, on repère immédiatement les erreurs susceptibles d'être générées par notre commande. En repoussant l'implémentation de ces 2 fonctions, on s'évite le calamiteux throw new Error("Error in login") par défaut qui imposera de tartiner des try/catch partout.

Dans notre lancée, codons la réservation pour faire passer notre test.