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