📐 Construire une app en JS
13. La classe App

La classe App

Comme toujours, revenons à notre test pour déterminer ce qu'il faut coder.

// 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({
        accommodationId:"accommodation-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.bookings.listBookingsForAccommodationId("accommodation-1")
expect(bookings).toHaveLength(1)

Notre classe App ne semble pas compliquée, puisqu'elle comporte :

  • un constructeur qui prend en paramètre le container des dépendances
  • une méthode run
  • un attribut public dependencies

Injection des dépendances

L'injection des dépendances dans l'app se fait facilement, puisque le container est passé au constructeur.

class App {
    constructor (dependencies) {
        this.dependencies = dependencies;
    }
    async run(usecases) {}
}

En revanche, la méthode run demande plus de ténacité.

Gestion du contexte

Souvenons-nous qu'une commande est une fonction qui retourne une fonction.

Par exemple pour login() :

function login(payload) {
    const {email, password} = payload;
    return async 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)
    }
}

On passe les paramètres de login et mot de passe à notre commande, et elle retourne une fonction asynchrone function(dependencies, context) qui retourne un contexte pour la commande suivante.

Pour les dépendances, le problème est réglé. Reste la question du contexte.

Toujours de la même façon, déduisons le code du contexte de notre test. On voit 2 méthodes withError et withUser.

class Context {
    withError (error) {
        this.error = error;
        return this;
    }
    withUser (user) {
        this.loggedUser = user;
        return this;
    }
}

Deux détails qui ont leur importance. La propriété s'appelle loggedUser, parce que c'est le nom que book() est allé lire au chapitre 11 — un contexte qui écrirait user laisserait book() avec un undefined. Et chaque méthode retourne this, sans quoi la commande suivante recevrait undefined en guise de contexte.

Désormais, on dispose du nécessaire pour coder notre classe App :

class App {
    constructor (dependencies) {
        this.dependencies = dependencies;
    }
    async run(usecases) {
        let context = new Context()
        for (const usecase of usecases) {
            // Les commandes attendent les dépendances et le contexte
            context = await usecase(this.dependencies, context)
        }
    }
}

Terminé ? Pas complètement.

Notre méthode run enchaîne toutes les commandes, même lorsqu'une erreur est survenue.

Ajoutons donc un fusible pour arrêter en cas d'erreur dans le contexte, et retournons le contexte pour que l'appelant puisse gérer l'erreur rencontrée.

class App {
    constructor (dependencies) {
        this.dependencies = dependencies;
    }
    async run(usecases) {
        let context = new Context()
        for (const usecase of usecases) {
            // Inutile de continuer en cas d'erreur
            if (context.isOk()) {
                context = await usecase(this.dependencies, context)
            }
        }
        return context
    }
}

Au passage, on a eu besoin d'une méthode isOk() dans la classe Context qu'il nous faut donc ajouter :

class Context {
    withError (error) {
        this.error = error;
        return this;
    }
    withUser (user) {
        this.loggedUser = user;
        return this;
    }
    isOk() {
        return !this.error
    }
}

Que reste-t-il sur notre liste ?

  • les dépendances
  • la classe App
  • le constructeur de l'objet Booking appelé dans la fonction book()

Codons la logique métier de notre objet Booking, seule brique manquante pour faire passer notre test.