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
Bookingappelé dans la fonctionbook()
Codons la logique métier de notre objet Booking, seule brique manquante pour faire passer notre test.